@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,184 @@
|
|
|
1
|
+
import { stringWidth } from "#/ansi/string-width.ts";
|
|
2
|
+
import {
|
|
3
|
+
type AnsiCode,
|
|
4
|
+
type StyledChar,
|
|
5
|
+
styledCharsFromTokens,
|
|
6
|
+
tokenize,
|
|
7
|
+
} from "#/ansi/tokenize.ts";
|
|
8
|
+
import {
|
|
9
|
+
cellAttributes,
|
|
10
|
+
createCell,
|
|
11
|
+
type Cell,
|
|
12
|
+
type CellStyle,
|
|
13
|
+
type Color,
|
|
14
|
+
type Hyperlink,
|
|
15
|
+
type UnderlineStyle,
|
|
16
|
+
} from "#/screen/cell.ts";
|
|
17
|
+
|
|
18
|
+
const ESC = "\u001B";
|
|
19
|
+
const BEL = "\u0007";
|
|
20
|
+
const C1_ST = "\u009C";
|
|
21
|
+
const LINK_PREFIX = `${ESC}]8;`;
|
|
22
|
+
|
|
23
|
+
type ParsedStyle = {
|
|
24
|
+
foreground?: Color;
|
|
25
|
+
background?: Color;
|
|
26
|
+
underlineColor?: Color;
|
|
27
|
+
underline: UnderlineStyle;
|
|
28
|
+
attributes: number;
|
|
29
|
+
hyperlink?: Hyperlink;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Converts the renderer's canonical ANSI state into backend-neutral cells. */
|
|
33
|
+
export function cellsFromAnsi(text: string): Cell[] {
|
|
34
|
+
return styledCharsFromTokens(tokenize(text)).map((character) => cellFromStyledChar(character));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cellFromStyledChar(
|
|
38
|
+
character: StyledChar,
|
|
39
|
+
width = stringWidth(character.value),
|
|
40
|
+
): Cell {
|
|
41
|
+
const parsed = parseStyles(character.styles);
|
|
42
|
+
const style: CellStyle = {
|
|
43
|
+
...(parsed.foreground ? { foreground: parsed.foreground } : {}),
|
|
44
|
+
...(parsed.background ? { background: parsed.background } : {}),
|
|
45
|
+
...(parsed.underlineColor ? { underlineColor: parsed.underlineColor } : {}),
|
|
46
|
+
underline: parsed.underline,
|
|
47
|
+
attributes: parsed.attributes,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return createCell(character.value, Math.max(1, width), style, parsed.hyperlink);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseStyles(codes: readonly AnsiCode[]): ParsedStyle {
|
|
54
|
+
let foreground: Color | undefined;
|
|
55
|
+
let background: Color | undefined;
|
|
56
|
+
let underlineColor: Color | undefined;
|
|
57
|
+
let underline: UnderlineStyle = "none";
|
|
58
|
+
let attributes = cellAttributes.none;
|
|
59
|
+
let hyperlink: Hyperlink | undefined;
|
|
60
|
+
|
|
61
|
+
for (const { code } of codes) {
|
|
62
|
+
if (code.startsWith(LINK_PREFIX)) {
|
|
63
|
+
hyperlink = parseHyperlink(code);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!code.startsWith(`${ESC}[`) || !code.endsWith("m")) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parameters = code.slice(2, -1);
|
|
72
|
+
const primary = Number.parseInt(parameters, 10);
|
|
73
|
+
|
|
74
|
+
if (primary >= 30 && primary <= 37) {
|
|
75
|
+
foreground = { model: "indexed", index: primary - 30, encoding: "ansi16" };
|
|
76
|
+
} else if (primary >= 90 && primary <= 97) {
|
|
77
|
+
foreground = { model: "indexed", index: primary - 90 + 8, encoding: "ansi16" };
|
|
78
|
+
} else if (primary >= 40 && primary <= 47) {
|
|
79
|
+
background = { model: "indexed", index: primary - 40, encoding: "ansi16" };
|
|
80
|
+
} else if (primary >= 100 && primary <= 107) {
|
|
81
|
+
background = { model: "indexed", index: primary - 100 + 8, encoding: "ansi16" };
|
|
82
|
+
} else if (primary === 38) {
|
|
83
|
+
foreground = parseExtendedColor(parameters);
|
|
84
|
+
} else if (primary === 48) {
|
|
85
|
+
background = parseExtendedColor(parameters);
|
|
86
|
+
} else if (primary === 58) {
|
|
87
|
+
underlineColor = parseExtendedColor(parameters);
|
|
88
|
+
} else if (primary === 1) {
|
|
89
|
+
attributes += cellAttributes.bold;
|
|
90
|
+
} else if (primary === 2) {
|
|
91
|
+
attributes += cellAttributes.faint;
|
|
92
|
+
} else if (primary === 3) {
|
|
93
|
+
attributes += cellAttributes.italic;
|
|
94
|
+
} else if (primary === 5) {
|
|
95
|
+
attributes += cellAttributes.blink;
|
|
96
|
+
} else if (primary === 6) {
|
|
97
|
+
attributes += cellAttributes.rapidBlink;
|
|
98
|
+
} else if (primary === 7) {
|
|
99
|
+
attributes += cellAttributes.inverse;
|
|
100
|
+
} else if (primary === 8) {
|
|
101
|
+
attributes += cellAttributes.hidden;
|
|
102
|
+
} else if (primary === 9) {
|
|
103
|
+
attributes += cellAttributes.strikethrough;
|
|
104
|
+
} else if (primary === 4) {
|
|
105
|
+
underline = parseUnderline(parameters);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...(foreground ? { foreground } : {}),
|
|
111
|
+
...(background ? { background } : {}),
|
|
112
|
+
...(underlineColor ? { underlineColor } : {}),
|
|
113
|
+
underline,
|
|
114
|
+
attributes,
|
|
115
|
+
...(hyperlink ? { hyperlink } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseExtendedColor(parameters: string): Color | undefined {
|
|
120
|
+
const parts = parameters.split(parameters.includes(":") ? ":" : ";");
|
|
121
|
+
const model = parts[1];
|
|
122
|
+
|
|
123
|
+
if (model === "5") {
|
|
124
|
+
const index = Number(parts[2]);
|
|
125
|
+
return Number.isInteger(index) && index >= 0 && index <= 255
|
|
126
|
+
? { model: "indexed", index, encoding: "ansi256" }
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (model !== "2") {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const channels = parts.slice(-3).map(Number);
|
|
135
|
+
if (channels.length !== 3 || channels.some((channel) => !isByte(channel))) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
model: "rgb",
|
|
141
|
+
red: channels[0]!,
|
|
142
|
+
green: channels[1]!,
|
|
143
|
+
blue: channels[2]!,
|
|
144
|
+
alpha: 255,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseUnderline(parameters: string): UnderlineStyle {
|
|
149
|
+
const variant = parameters.includes(":") ? Number(parameters.split(":").at(-1)) : 1;
|
|
150
|
+
return (
|
|
151
|
+
(
|
|
152
|
+
{
|
|
153
|
+
0: "none",
|
|
154
|
+
1: "single",
|
|
155
|
+
2: "double",
|
|
156
|
+
3: "curly",
|
|
157
|
+
4: "dotted",
|
|
158
|
+
5: "dashed",
|
|
159
|
+
} as const
|
|
160
|
+
)[variant] ?? "single"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseHyperlink(code: string): Hyperlink | undefined {
|
|
165
|
+
let body = code.slice(LINK_PREFIX.length);
|
|
166
|
+
if (body.endsWith(`${ESC}\\`)) {
|
|
167
|
+
body = body.slice(0, -2);
|
|
168
|
+
} else if (body.endsWith(BEL) || body.endsWith(C1_ST)) {
|
|
169
|
+
body = body.slice(0, -1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const separator = body.indexOf(";");
|
|
173
|
+
if (separator < 0) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parameters = body.slice(0, separator);
|
|
178
|
+
const url = body.slice(separator + 1);
|
|
179
|
+
return parameters ? { url, parameters } : { url };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isByte(value: number): boolean {
|
|
183
|
+
return Number.isInteger(value) && value >= 0 && value <= 255;
|
|
184
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { sliceAnsi } from "#/ansi/slice.ts";
|
|
2
|
+
import { stringWidth } from "#/ansi/string-width.ts";
|
|
3
|
+
import type { StyledChar } from "#/ansi/tokenize.ts";
|
|
4
|
+
import type { PaintContext } from "#/color/paint.ts";
|
|
5
|
+
import { cellFromStyledChar, Screen, type Cell, type ColorProfile } from "#/screen/index.ts";
|
|
6
|
+
import type { AnsiTransformer } from "#/transform-adapter.ts";
|
|
7
|
+
import { transformAnsiLine } from "#/transform-adapter.ts";
|
|
8
|
+
|
|
9
|
+
type Options = {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
colorProfile?: ColorProfile;
|
|
13
|
+
paintContext?: PaintContext;
|
|
14
|
+
};
|
|
15
|
+
type Clip = {
|
|
16
|
+
x1: number | undefined;
|
|
17
|
+
x2: number | undefined;
|
|
18
|
+
y1: number | undefined;
|
|
19
|
+
y2: number | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Immediate structured drawing canvas with one explicit ANSI compatibility adapter. */
|
|
23
|
+
export class Canvas {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
readonly paintContext: PaintContext;
|
|
27
|
+
readonly #screen: Screen;
|
|
28
|
+
readonly #clips: Clip[] = [];
|
|
29
|
+
|
|
30
|
+
constructor(options: Options) {
|
|
31
|
+
this.width = options.width;
|
|
32
|
+
this.height = options.height;
|
|
33
|
+
this.paintContext = { profile: options.colorProfile ?? "truecolor", ...options.paintContext };
|
|
34
|
+
this.#screen = new Screen(this.width, this.height);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
writeCells(
|
|
38
|
+
x: number,
|
|
39
|
+
y: number,
|
|
40
|
+
lines: readonly (readonly Cell[])[],
|
|
41
|
+
options?: { overflow?: boolean },
|
|
42
|
+
): void {
|
|
43
|
+
const clip = this.#clips.at(-1);
|
|
44
|
+
const overflow = options?.overflow === true;
|
|
45
|
+
for (const [rowOffset, line] of lines.entries()) {
|
|
46
|
+
const currentY = y + rowOffset;
|
|
47
|
+
if (!this.#insideY(currentY, clip)) continue;
|
|
48
|
+
let currentX = x;
|
|
49
|
+
for (const cell of line) {
|
|
50
|
+
const endX = currentX + cell.width;
|
|
51
|
+
const clipped =
|
|
52
|
+
(clip?.x1 !== undefined && currentX < clip.x1) ||
|
|
53
|
+
(clip?.x2 !== undefined && endX > clip.x2);
|
|
54
|
+
if (!clipped) this.#composeNativeCell(currentX, currentY, cell, overflow);
|
|
55
|
+
currentX = endX;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writeAnsi(
|
|
61
|
+
x: number,
|
|
62
|
+
y: number,
|
|
63
|
+
text: string,
|
|
64
|
+
options: { transformers: AnsiTransformer[]; overflow?: boolean },
|
|
65
|
+
): void {
|
|
66
|
+
if (!text) return;
|
|
67
|
+
let lines = text.split("\n");
|
|
68
|
+
const clip = this.#clips.at(-1);
|
|
69
|
+
const overflow = options.overflow === true;
|
|
70
|
+
if (clip?.y1 !== undefined && y < clip.y1) {
|
|
71
|
+
lines = lines.slice(clip.y1 - y);
|
|
72
|
+
y = clip.y1;
|
|
73
|
+
}
|
|
74
|
+
if (clip?.y2 !== undefined) lines = lines.slice(0, Math.max(0, clip.y2 - y));
|
|
75
|
+
for (const [lineIndex, original] of lines.entries()) {
|
|
76
|
+
const currentY = y + lineIndex;
|
|
77
|
+
if (!this.#insideY(currentY, clip)) continue;
|
|
78
|
+
let line = original;
|
|
79
|
+
let currentX = x;
|
|
80
|
+
if (clip?.x1 !== undefined && currentX < clip.x1) {
|
|
81
|
+
line = sliceAnsi(line, clip.x1 - currentX);
|
|
82
|
+
currentX = clip.x1;
|
|
83
|
+
}
|
|
84
|
+
if (clip?.x2 !== undefined) line = sliceAnsi(line, 0, Math.max(0, clip.x2 - currentX));
|
|
85
|
+
for (const character of transformAnsiLine(line, lineIndex, options.transformers)) {
|
|
86
|
+
const width = Math.max(1, stringWidth(character.value));
|
|
87
|
+
if (clip?.x2 !== undefined && currentX + width > clip.x2) break;
|
|
88
|
+
this.#writeCompatibilityCell(currentX, currentY, character, width, overflow);
|
|
89
|
+
currentX += width;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clip(clip: Clip): void {
|
|
95
|
+
this.#clips.push(clip);
|
|
96
|
+
}
|
|
97
|
+
unclip(): void {
|
|
98
|
+
this.#clips.pop();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
finish(): Screen {
|
|
102
|
+
return this.#screen;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#insideY(y: number, clip: Clip | undefined): boolean {
|
|
106
|
+
return (
|
|
107
|
+
y >= 0 &&
|
|
108
|
+
y < this.#screen.height &&
|
|
109
|
+
(clip?.y1 === undefined || y >= clip.y1) &&
|
|
110
|
+
(clip?.y2 === undefined || y < clip.y2)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#composeNativeCell(x: number, y: number, cell: Cell, overflow = false): void {
|
|
115
|
+
if (!overflow && x >= this.#screen.width) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.#screen.composeCell(
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
{
|
|
122
|
+
content: { grapheme: cell.grapheme, width: cell.width },
|
|
123
|
+
foreground: cell.reset?.foreground ? null : (cell.style.foreground ?? null),
|
|
124
|
+
background: cell.reset?.background ? null : cell.style.background,
|
|
125
|
+
underlineColor: cell.style.underlineColor ?? null,
|
|
126
|
+
underline: cell.style.underline,
|
|
127
|
+
attributes: cell.style.attributes,
|
|
128
|
+
hyperlink: cell.hyperlink ?? null,
|
|
129
|
+
},
|
|
130
|
+
{ overflow },
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#writeCompatibilityCell(
|
|
135
|
+
x: number,
|
|
136
|
+
y: number,
|
|
137
|
+
character: StyledChar,
|
|
138
|
+
width: number,
|
|
139
|
+
overflow = false,
|
|
140
|
+
): void {
|
|
141
|
+
if (!overflow && x >= this.#screen.width) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const cell = cellFromStyledChar(character, width);
|
|
145
|
+
this.#screen.composeCell(
|
|
146
|
+
x,
|
|
147
|
+
y,
|
|
148
|
+
{
|
|
149
|
+
content: { grapheme: cell.grapheme, width: cell.width },
|
|
150
|
+
foreground: cell.style.foreground ?? null,
|
|
151
|
+
background: cell.style.background,
|
|
152
|
+
underlineColor: cell.style.underlineColor ?? null,
|
|
153
|
+
underline: cell.style.underline,
|
|
154
|
+
attributes: cell.style.attributes,
|
|
155
|
+
hyperlink: cell.hyperlink ?? null,
|
|
156
|
+
},
|
|
157
|
+
{ overflow },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A terminal color before it is converted to an output profile.
|
|
3
|
+
*/
|
|
4
|
+
export type Color = RgbColor | IndexedColor;
|
|
5
|
+
|
|
6
|
+
export type RgbColor = {
|
|
7
|
+
readonly model: "rgb";
|
|
8
|
+
readonly red: number;
|
|
9
|
+
readonly green: number;
|
|
10
|
+
readonly blue: number;
|
|
11
|
+
readonly alpha: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type IndexedColor = {
|
|
15
|
+
readonly model: "indexed";
|
|
16
|
+
readonly index: number;
|
|
17
|
+
/** Preserve an explicit palette encoding when compatibility requires it. */
|
|
18
|
+
readonly encoding?: "ansi16" | "ansi256";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
Bit flags for terminal text attributes. Multiple attributes may be combined.
|
|
23
|
+
*/
|
|
24
|
+
export const cellAttributes = {
|
|
25
|
+
none: 0,
|
|
26
|
+
bold: 1 << 0,
|
|
27
|
+
faint: 1 << 1,
|
|
28
|
+
italic: 1 << 2,
|
|
29
|
+
blink: 1 << 3,
|
|
30
|
+
rapidBlink: 1 << 4,
|
|
31
|
+
inverse: 1 << 5,
|
|
32
|
+
hidden: 1 << 6,
|
|
33
|
+
strikethrough: 1 << 7,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type UnderlineStyle = "none" | "single" | "double" | "curly" | "dotted" | "dashed";
|
|
37
|
+
|
|
38
|
+
export type CellStyle = {
|
|
39
|
+
readonly foreground?: Color;
|
|
40
|
+
readonly background?: Color;
|
|
41
|
+
readonly underlineColor?: Color;
|
|
42
|
+
readonly underline: UnderlineStyle;
|
|
43
|
+
readonly attributes: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type Hyperlink = {
|
|
47
|
+
readonly url: string;
|
|
48
|
+
readonly parameters?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type CellContent = {
|
|
52
|
+
readonly grapheme: string;
|
|
53
|
+
readonly width: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
A compositing operation for one screen position.
|
|
58
|
+
|
|
59
|
+
An absent field is transparent and preserves the destination channel. `null`
|
|
60
|
+
explicitly resets a color or hyperlink to the terminal default. Supplying
|
|
61
|
+
`content` paints a grapheme; `{grapheme: " ", width: 1}` is therefore an
|
|
62
|
+
explicit blank, while an absent patch paints nothing at all.
|
|
63
|
+
*/
|
|
64
|
+
export type CellPatch = {
|
|
65
|
+
readonly content?: CellContent;
|
|
66
|
+
readonly foreground?: Color | null;
|
|
67
|
+
readonly background?: Color | null;
|
|
68
|
+
readonly underlineColor?: Color | null;
|
|
69
|
+
readonly underline?: UnderlineStyle;
|
|
70
|
+
readonly attributes?: number;
|
|
71
|
+
readonly hyperlink?: Hyperlink | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
One grapheme cluster in a terminal screen. A width of zero is reserved for the
|
|
76
|
+
continuation columns of a wide grapheme and is created only by `Screen`.
|
|
77
|
+
*/
|
|
78
|
+
export type Cell = {
|
|
79
|
+
readonly grapheme: string;
|
|
80
|
+
readonly width: number;
|
|
81
|
+
readonly style: CellStyle;
|
|
82
|
+
readonly hyperlink?: Hyperlink;
|
|
83
|
+
/** Composition intent consumed when this cell is drawn over another surface. */
|
|
84
|
+
readonly reset?: { readonly foreground?: boolean; readonly background?: boolean };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const emptyCellStyle: CellStyle = {
|
|
88
|
+
underline: "none",
|
|
89
|
+
attributes: cellAttributes.none,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const emptyCell: Cell = {
|
|
93
|
+
grapheme: " ",
|
|
94
|
+
width: 1,
|
|
95
|
+
style: emptyCellStyle,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const createCell = (
|
|
99
|
+
grapheme: string,
|
|
100
|
+
width: number,
|
|
101
|
+
style: CellStyle = emptyCellStyle,
|
|
102
|
+
hyperlink?: Hyperlink,
|
|
103
|
+
reset?: Cell["reset"],
|
|
104
|
+
): Cell => {
|
|
105
|
+
if (grapheme.length === 0) {
|
|
106
|
+
throw new Error("A cell must contain one grapheme cluster");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!Number.isInteger(width) || width < 1) {
|
|
110
|
+
throw new Error("A cell width must be a positive integer");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
grapheme,
|
|
115
|
+
width,
|
|
116
|
+
style,
|
|
117
|
+
...(hyperlink ? { hyperlink } : {}),
|
|
118
|
+
...(reset ? { reset } : {}),
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const cellsEqual = (left: Cell | undefined, right: Cell | undefined): boolean => {
|
|
123
|
+
if (left === right) return true;
|
|
124
|
+
if (!left || !right || left.grapheme !== right.grapheme || left.width !== right.width) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const leftStyle = left.style;
|
|
129
|
+
const rightStyle = right.style;
|
|
130
|
+
return (
|
|
131
|
+
leftStyle.attributes === rightStyle.attributes &&
|
|
132
|
+
leftStyle.underline === rightStyle.underline &&
|
|
133
|
+
JSON.stringify(leftStyle.foreground) === JSON.stringify(rightStyle.foreground) &&
|
|
134
|
+
JSON.stringify(leftStyle.background) === JSON.stringify(rightStyle.background) &&
|
|
135
|
+
JSON.stringify(leftStyle.underlineColor) === JSON.stringify(rightStyle.underlineColor) &&
|
|
136
|
+
JSON.stringify(left.hyperlink) === JSON.stringify(right.hyperlink)
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ansi256ToAnsi, rgbToAnsi, rgbToAnsi256 } from "#/ansi/sgr.ts";
|
|
2
|
+
import type { ColorSupportLevel } from "#/capabilities/detect.ts";
|
|
3
|
+
import type { Color } from "#/screen/cell.ts";
|
|
4
|
+
|
|
5
|
+
/** The color model emitted by a terminal serializer. */
|
|
6
|
+
export type ColorProfile = "none" | "ansi16" | "ansi256" | "truecolor";
|
|
7
|
+
|
|
8
|
+
export function colorProfileFromLevel(level: ColorSupportLevel): ColorProfile {
|
|
9
|
+
return (["none", "ansi16", "ansi256", "truecolor"] as const)[level];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
Quantizes a semantic color for an output profile. `undefined` means the
|
|
14
|
+
terminal default and is also the result for the no-color profile.
|
|
15
|
+
*/
|
|
16
|
+
export function quantizeColor(color: Color | undefined, profile: ColorProfile): Color | undefined {
|
|
17
|
+
if (!color || profile === "none") {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (profile === "truecolor") {
|
|
22
|
+
return color;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (profile === "ansi256") {
|
|
26
|
+
if (color.model === "indexed") {
|
|
27
|
+
return color;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
model: "indexed",
|
|
32
|
+
index: rgbToAnsi256(color.red, color.green, color.blue),
|
|
33
|
+
encoding: "ansi256",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ansiCode =
|
|
38
|
+
color.model === "indexed"
|
|
39
|
+
? ansi256ToAnsi(color.index)
|
|
40
|
+
: rgbToAnsi(color.red, color.green, color.blue);
|
|
41
|
+
|
|
42
|
+
return { model: "indexed", index: ansiCodeToPaletteIndex(ansiCode), encoding: "ansi16" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ansiCodeToPaletteIndex(code: number): number {
|
|
46
|
+
return code >= 90 ? code - 90 + 8 : code - 30;
|
|
47
|
+
}
|