@swifttui/web 0.0.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/AGENTS.md +52 -0
- package/README.md +116 -0
- package/cli.ts +168 -0
- package/index.html +50 -0
- package/index.ts +8 -0
- package/manifest.ts +1 -0
- package/package.json +33 -0
- package/src/AccessibilityTree.ts +262 -0
- package/src/BoxDrawingRenderer.ts +585 -0
- package/src/PublicEntrypointBoundary.test.ts +20 -0
- package/src/WebHostApp.test.ts +222 -0
- package/src/WebHostApp.ts +269 -0
- package/src/WebHostSceneManifest.test.ts +38 -0
- package/src/WebHostSceneManifest.ts +156 -0
- package/src/WebHostSceneRuntime.test.ts +1752 -0
- package/src/WebHostSceneRuntime.ts +955 -0
- package/src/WebHostSurfaceTransport.test.ts +362 -0
- package/src/WebHostSurfaceTransport.ts +648 -0
- package/src/WebHostTerminalStyle.test.ts +123 -0
- package/src/WebHostTerminalStyle.ts +471 -0
- package/src/WebHostTestFixtures.ts +10 -0
- package/src/WebSocketSceneBridge.test.ts +198 -0
- package/src/WebSocketSceneBridge.ts +233 -0
- package/src/browser.ts +59 -0
- package/src/wasi/BrowserWASIBridge.test.ts +168 -0
- package/src/wasi/BrowserWASIBridge.ts +167 -0
- package/src/wasi/SharedInputQueue.test.ts +146 -0
- package/src/wasi/SharedInputQueue.ts +199 -0
- package/src/wasi/StdIOPipe.ts +72 -0
- package/src/wasi/WasiPollScheduler.test.ts +176 -0
- package/src/wasi/WasiPollScheduler.ts +305 -0
- package/src/wasi/WasmSceneRuntime.ts +205 -0
- package/src/wasi/WasmSceneWorker.ts +182 -0
- package/style.css +15 -0
- package/testing.ts +1 -0
- package/tsconfig.json +29 -0
- package/wasi-worker.ts +1 -0
- package/wasi.ts +4 -0
- package/websocket.ts +1 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
decodeWebHostTerminalRenderStyleBase64,
|
|
6
|
+
encodeWebHostTerminalRenderStyleBase64,
|
|
7
|
+
normalizeWebHostTerminalStyle,
|
|
8
|
+
resolveWebHostTerminalRenderStyle,
|
|
9
|
+
webTUITerminalBackgroundColor,
|
|
10
|
+
} from "./WebHostTerminalStyle.ts";
|
|
11
|
+
|
|
12
|
+
test("terminal style normalization fills default palette and theme", () => {
|
|
13
|
+
const style = normalizeWebHostTerminalStyle({
|
|
14
|
+
fontSize: 16,
|
|
15
|
+
cursorBlink: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(style.fontSize).toBe(16);
|
|
19
|
+
expect(style.cursorBlink).toBe(true);
|
|
20
|
+
expect(style.fontFamily.length).toBeGreaterThan(0);
|
|
21
|
+
expect(style.theme.background).toBe("#1e222a");
|
|
22
|
+
expect(style.palette.background).toBe("#1e222a");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("terminal style resolves host-owned theme payloads", () => {
|
|
26
|
+
const style = {
|
|
27
|
+
palette: {
|
|
28
|
+
foreground: "#101010",
|
|
29
|
+
background: "#fafafa",
|
|
30
|
+
cursor: "#101010",
|
|
31
|
+
selectionBackground: "#d0e4ff",
|
|
32
|
+
selectionForeground: "#101010",
|
|
33
|
+
},
|
|
34
|
+
theme: {
|
|
35
|
+
foreground: "#111111",
|
|
36
|
+
background: "#fafafa",
|
|
37
|
+
tint: "#0f62fe",
|
|
38
|
+
separator: "#d0d0d0",
|
|
39
|
+
selection: "#d0e4ff",
|
|
40
|
+
placeholder: "#707070",
|
|
41
|
+
link: "#0f62fe",
|
|
42
|
+
fill: "#f4f4f4",
|
|
43
|
+
windowBackground: "#ffffff",
|
|
44
|
+
success: "#198038",
|
|
45
|
+
warning: "#b46e00",
|
|
46
|
+
danger: "#da1e28",
|
|
47
|
+
info: "#0f62fe",
|
|
48
|
+
muted: "#525252",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const resolved = resolveWebHostTerminalRenderStyle(style);
|
|
53
|
+
expect(resolved.appearance.foregroundColor).toBe("#111111");
|
|
54
|
+
expect(resolved.appearance.backgroundColor).toBe("#fafafa");
|
|
55
|
+
expect(resolved.theme?.warning).toBe("#b46e00");
|
|
56
|
+
expect(encodeWebHostTerminalRenderStyleBase64(style)).toBeDefined();
|
|
57
|
+
expect(
|
|
58
|
+
decodeWebHostTerminalRenderStyleBase64(
|
|
59
|
+
encodeWebHostTerminalRenderStyleBase64(style)
|
|
60
|
+
)?.appearance.backgroundColor
|
|
61
|
+
).toBe("#fafafa");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("terminal style maps to surface palette and translucent background", () => {
|
|
65
|
+
const style = {
|
|
66
|
+
backgroundOpacity: 0.5,
|
|
67
|
+
palette: {
|
|
68
|
+
foreground: "#ededed",
|
|
69
|
+
background: "#202020",
|
|
70
|
+
cursor: "#ffffff",
|
|
71
|
+
selectionBackground: "#264f78",
|
|
72
|
+
selectionForeground: "#ffffff",
|
|
73
|
+
},
|
|
74
|
+
theme: {
|
|
75
|
+
foreground: "#ededed",
|
|
76
|
+
background: "#202020",
|
|
77
|
+
tint: "#56b6c2",
|
|
78
|
+
separator: "#4c566a",
|
|
79
|
+
selection: "#2e3440",
|
|
80
|
+
placeholder: "#8c92ac",
|
|
81
|
+
link: "#5ba3ff",
|
|
82
|
+
fill: "#2b303b",
|
|
83
|
+
windowBackground: "#15181e",
|
|
84
|
+
success: "#61c67b",
|
|
85
|
+
warning: "#ebb33c",
|
|
86
|
+
danger: "#e05757",
|
|
87
|
+
info: "#56b6c2",
|
|
88
|
+
muted: "#8c92ac",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
expect(normalizeWebHostTerminalStyle(style).palette.foreground).toBe("#ededed");
|
|
93
|
+
expect(normalizeWebHostTerminalStyle(style).palette.background).toBe("#202020");
|
|
94
|
+
expect(webTUITerminalBackgroundColor(style)).toBe("rgba(32, 32, 32, 0.5)");
|
|
95
|
+
expect(resolveWebHostTerminalRenderStyle(style).appearance.palette["0"]).toBe("#20242c");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("shared default transport fixtures stay in sync with WebHost encoding", () => {
|
|
99
|
+
const fixture = transportFixture("terminal-render-style-default");
|
|
100
|
+
|
|
101
|
+
expect(JSON.stringify(resolveWebHostTerminalRenderStyle({}))).toBe(fixture.json);
|
|
102
|
+
expect(encodeWebHostTerminalRenderStyleBase64({})).toBe(fixture.base64);
|
|
103
|
+
expect(
|
|
104
|
+
JSON.stringify(decodeWebHostTerminalRenderStyleBase64(fixture.base64))
|
|
105
|
+
).toBe(fixture.json);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function transportFixture(
|
|
109
|
+
basename: string
|
|
110
|
+
): { json: string; base64: string } {
|
|
111
|
+
const json = readTransportFixture(`${basename}.json`);
|
|
112
|
+
const base64 = readTransportFixture(`${basename}.base64.txt`);
|
|
113
|
+
return { json, base64 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readTransportFixture(
|
|
117
|
+
name: string
|
|
118
|
+
): string {
|
|
119
|
+
return readFileSync(
|
|
120
|
+
new URL(`../../../Fixtures/Transport/${name}`, import.meta.url),
|
|
121
|
+
"utf8"
|
|
122
|
+
).trim();
|
|
123
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
export type WebHostTerminalCursorStyle = "block" | "bar" | "underline";
|
|
2
|
+
|
|
3
|
+
export interface WebHostANSIColors {
|
|
4
|
+
black?: string;
|
|
5
|
+
red?: string;
|
|
6
|
+
green?: string;
|
|
7
|
+
yellow?: string;
|
|
8
|
+
blue?: string;
|
|
9
|
+
magenta?: string;
|
|
10
|
+
cyan?: string;
|
|
11
|
+
white?: string;
|
|
12
|
+
brightBlack?: string;
|
|
13
|
+
brightRed?: string;
|
|
14
|
+
brightGreen?: string;
|
|
15
|
+
brightYellow?: string;
|
|
16
|
+
brightBlue?: string;
|
|
17
|
+
brightMagenta?: string;
|
|
18
|
+
brightCyan?: string;
|
|
19
|
+
brightWhite?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WebHostTerminalPalette {
|
|
23
|
+
foreground?: string;
|
|
24
|
+
background?: string;
|
|
25
|
+
cursor?: string;
|
|
26
|
+
selectionBackground?: string;
|
|
27
|
+
selectionForeground?: string;
|
|
28
|
+
ansi?: WebHostANSIColors;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WebHostTerminalTheme {
|
|
32
|
+
foreground?: string;
|
|
33
|
+
background?: string;
|
|
34
|
+
tint?: string;
|
|
35
|
+
separator?: string;
|
|
36
|
+
selection?: string;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
link?: string;
|
|
39
|
+
fill?: string;
|
|
40
|
+
windowBackground?: string;
|
|
41
|
+
success?: string;
|
|
42
|
+
warning?: string;
|
|
43
|
+
danger?: string;
|
|
44
|
+
info?: string;
|
|
45
|
+
muted?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WebHostTerminalStyle {
|
|
49
|
+
fontSize?: number;
|
|
50
|
+
fontFamily?: string;
|
|
51
|
+
cursorStyle?: WebHostTerminalCursorStyle;
|
|
52
|
+
cursorBlink?: boolean;
|
|
53
|
+
backgroundOpacity?: number;
|
|
54
|
+
palette?: WebHostTerminalPalette;
|
|
55
|
+
theme?: WebHostTerminalTheme;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ResolvedWebHostTerminalPalette {
|
|
59
|
+
foreground: string;
|
|
60
|
+
background: string;
|
|
61
|
+
cursor: string;
|
|
62
|
+
selectionBackground: string;
|
|
63
|
+
selectionForeground: string;
|
|
64
|
+
ansi: Required<WebHostANSIColors>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ResolvedWebHostTerminalStyle {
|
|
68
|
+
fontSize: number;
|
|
69
|
+
fontFamily: string;
|
|
70
|
+
cursorStyle: WebHostTerminalCursorStyle;
|
|
71
|
+
cursorBlink: boolean;
|
|
72
|
+
backgroundOpacity: number;
|
|
73
|
+
palette: ResolvedWebHostTerminalPalette;
|
|
74
|
+
theme: Required<WebHostTerminalTheme>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface WebHostTerminalRenderStyle {
|
|
78
|
+
appearance: WebHostTerminalAppearance;
|
|
79
|
+
theme?: WebHostTerminalTheme;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface WebHostTerminalAppearance {
|
|
83
|
+
foregroundColor: string;
|
|
84
|
+
backgroundColor: string;
|
|
85
|
+
tintColor: string;
|
|
86
|
+
palette: Record<string, string>;
|
|
87
|
+
colorSchemeContrast: "standard" | "increased";
|
|
88
|
+
source: "activeQuery" | "environmentHeuristics" | "fallback" | "override";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const defaultFontFamily =
|
|
92
|
+
'"SFMono-Regular", "SF Mono", "Menlo", "Monaco", "Consolas", "Liberation Mono", monospace';
|
|
93
|
+
|
|
94
|
+
const defaultANSI: Required<WebHostANSIColors> = {
|
|
95
|
+
black: "#20242c",
|
|
96
|
+
red: "#e05757",
|
|
97
|
+
green: "#61c67b",
|
|
98
|
+
yellow: "#ebb33c",
|
|
99
|
+
blue: "#5ba3ff",
|
|
100
|
+
magenta: "#b46eff",
|
|
101
|
+
cyan: "#56b6c2",
|
|
102
|
+
white: "#eceff4",
|
|
103
|
+
brightBlack: "#8c92ac",
|
|
104
|
+
brightRed: "#ff7b72",
|
|
105
|
+
brightGreen: "#7ee787",
|
|
106
|
+
brightYellow: "#f2cc60",
|
|
107
|
+
brightBlue: "#79c0ff",
|
|
108
|
+
brightMagenta: "#d2a8ff",
|
|
109
|
+
brightCyan: "#7de2d1",
|
|
110
|
+
brightWhite: "#ffffff",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const defaultPalette: ResolvedWebHostTerminalPalette = {
|
|
114
|
+
foreground: "#eceff4",
|
|
115
|
+
background: "#1e222a",
|
|
116
|
+
cursor: "#56b6c2",
|
|
117
|
+
selectionBackground: "#2e3440",
|
|
118
|
+
selectionForeground: "#eceff4",
|
|
119
|
+
ansi: defaultANSI,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const defaultTheme: Required<WebHostTerminalTheme> = {
|
|
123
|
+
foreground: "#eceff4",
|
|
124
|
+
background: "#1e222a",
|
|
125
|
+
tint: "#56b6c2",
|
|
126
|
+
separator: "#4c566a",
|
|
127
|
+
selection: "#2e3440",
|
|
128
|
+
placeholder: "#8c92ac",
|
|
129
|
+
link: "#5ba3ff",
|
|
130
|
+
fill: "#2b303b",
|
|
131
|
+
windowBackground: "#15181e",
|
|
132
|
+
success: "#61c67b",
|
|
133
|
+
warning: "#ebb33c",
|
|
134
|
+
danger: "#e05757",
|
|
135
|
+
info: "#56b6c2",
|
|
136
|
+
muted: "#8c92ac",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function normalizeWebHostTerminalStyle(
|
|
140
|
+
style: WebHostTerminalStyle = {}
|
|
141
|
+
): ResolvedWebHostTerminalStyle {
|
|
142
|
+
const palette = normalizePalette(style.palette, defaultPalette);
|
|
143
|
+
const theme = normalizeTheme(style.theme, palette, defaultTheme);
|
|
144
|
+
return {
|
|
145
|
+
fontSize: normalizeFontSize(style.fontSize ?? 14),
|
|
146
|
+
fontFamily: style.fontFamily ?? defaultFontFamily,
|
|
147
|
+
cursorStyle: style.cursorStyle ?? "block",
|
|
148
|
+
cursorBlink: style.cursorBlink ?? false,
|
|
149
|
+
backgroundOpacity: normalizeOpacity(style.backgroundOpacity ?? 1),
|
|
150
|
+
palette,
|
|
151
|
+
theme,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function mergeWebHostTerminalStyle(
|
|
156
|
+
base: WebHostTerminalStyle,
|
|
157
|
+
patch: WebHostTerminalStyle
|
|
158
|
+
): ResolvedWebHostTerminalStyle {
|
|
159
|
+
const resolvedBase = normalizeWebHostTerminalStyle(base);
|
|
160
|
+
return normalizeWebHostTerminalStyle({
|
|
161
|
+
...resolvedBase,
|
|
162
|
+
...patch,
|
|
163
|
+
palette: mergePalette(resolvedBase.palette, patch.palette),
|
|
164
|
+
theme: patch.theme ? { ...resolvedBase.theme, ...patch.theme } : resolvedBase.theme,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolveWebHostTerminalRenderStyle(
|
|
169
|
+
style: WebHostTerminalStyle
|
|
170
|
+
): WebHostTerminalRenderStyle {
|
|
171
|
+
const normalized = normalizeWebHostTerminalStyle(style);
|
|
172
|
+
return {
|
|
173
|
+
appearance: {
|
|
174
|
+
foregroundColor: normalized.theme.foreground,
|
|
175
|
+
backgroundColor: normalized.theme.background,
|
|
176
|
+
tintColor: normalized.theme.tint,
|
|
177
|
+
palette: paletteToIndexedMap(normalized.palette.ansi),
|
|
178
|
+
colorSchemeContrast: contrastRatio(normalized.theme.foreground, normalized.theme.background) >= 7
|
|
179
|
+
? "increased"
|
|
180
|
+
: "standard",
|
|
181
|
+
source: "override",
|
|
182
|
+
},
|
|
183
|
+
theme: { ...normalized.theme },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function encodeWebHostTerminalRenderStyleBase64(
|
|
188
|
+
style: WebHostTerminalStyle
|
|
189
|
+
): string {
|
|
190
|
+
return encodeBase64(JSON.stringify(resolveWebHostTerminalRenderStyle(style)));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function decodeWebHostTerminalRenderStyleBase64(
|
|
194
|
+
encoded: string
|
|
195
|
+
): WebHostTerminalRenderStyle | undefined {
|
|
196
|
+
const json = decodeBase64(encoded);
|
|
197
|
+
if (!json) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(json) as WebHostTerminalRenderStyle;
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function webTUITerminalBackgroundColor(
|
|
209
|
+
style: WebHostTerminalStyle
|
|
210
|
+
): string {
|
|
211
|
+
const normalized = normalizeWebHostTerminalStyle(style);
|
|
212
|
+
return hexToRgba(normalized.theme.background, normalized.backgroundOpacity);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function applyWebHostTerminalStyle(
|
|
216
|
+
element: HTMLElement,
|
|
217
|
+
style: WebHostTerminalStyle
|
|
218
|
+
): void {
|
|
219
|
+
const normalized = normalizeWebHostTerminalStyle(style);
|
|
220
|
+
element.style.fontFamily = normalized.fontFamily;
|
|
221
|
+
element.style.fontSize = `${normalized.fontSize}px`;
|
|
222
|
+
element.style.background = hexToRgba(normalized.theme.background, normalized.backgroundOpacity);
|
|
223
|
+
element.style.color = normalized.theme.foreground;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizePalette(
|
|
227
|
+
input: WebHostTerminalPalette | undefined,
|
|
228
|
+
defaults: ResolvedWebHostTerminalPalette
|
|
229
|
+
): ResolvedWebHostTerminalPalette {
|
|
230
|
+
return {
|
|
231
|
+
foreground: normalizeHexColor(input?.foreground ?? defaults.foreground),
|
|
232
|
+
background: normalizeHexColor(input?.background ?? defaults.background),
|
|
233
|
+
cursor: normalizeHexColor(input?.cursor ?? defaults.cursor),
|
|
234
|
+
selectionBackground: normalizeHexColor(
|
|
235
|
+
input?.selectionBackground ?? defaults.selectionBackground
|
|
236
|
+
),
|
|
237
|
+
selectionForeground: normalizeHexColor(
|
|
238
|
+
input?.selectionForeground ?? defaults.selectionForeground
|
|
239
|
+
),
|
|
240
|
+
ansi: normalizeANSI(input?.ansi, defaults.ansi),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function mergePalette(
|
|
245
|
+
base: ResolvedWebHostTerminalPalette,
|
|
246
|
+
patch: WebHostTerminalPalette | undefined
|
|
247
|
+
): WebHostTerminalPalette {
|
|
248
|
+
if (!patch) {
|
|
249
|
+
return base;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
...base,
|
|
254
|
+
...patch,
|
|
255
|
+
ansi: patch.ansi ? { ...base.ansi, ...patch.ansi } : base.ansi,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeANSI(
|
|
260
|
+
input: WebHostANSIColors | undefined,
|
|
261
|
+
defaults: Required<WebHostANSIColors>
|
|
262
|
+
): Required<WebHostANSIColors> {
|
|
263
|
+
return {
|
|
264
|
+
black: normalizeHexColor(input?.black ?? defaults.black),
|
|
265
|
+
red: normalizeHexColor(input?.red ?? defaults.red),
|
|
266
|
+
green: normalizeHexColor(input?.green ?? defaults.green),
|
|
267
|
+
yellow: normalizeHexColor(input?.yellow ?? defaults.yellow),
|
|
268
|
+
blue: normalizeHexColor(input?.blue ?? defaults.blue),
|
|
269
|
+
magenta: normalizeHexColor(input?.magenta ?? defaults.magenta),
|
|
270
|
+
cyan: normalizeHexColor(input?.cyan ?? defaults.cyan),
|
|
271
|
+
white: normalizeHexColor(input?.white ?? defaults.white),
|
|
272
|
+
brightBlack: normalizeHexColor(input?.brightBlack ?? defaults.brightBlack),
|
|
273
|
+
brightRed: normalizeHexColor(input?.brightRed ?? defaults.brightRed),
|
|
274
|
+
brightGreen: normalizeHexColor(input?.brightGreen ?? defaults.brightGreen),
|
|
275
|
+
brightYellow: normalizeHexColor(input?.brightYellow ?? defaults.brightYellow),
|
|
276
|
+
brightBlue: normalizeHexColor(input?.brightBlue ?? defaults.brightBlue),
|
|
277
|
+
brightMagenta: normalizeHexColor(input?.brightMagenta ?? defaults.brightMagenta),
|
|
278
|
+
brightCyan: normalizeHexColor(input?.brightCyan ?? defaults.brightCyan),
|
|
279
|
+
brightWhite: normalizeHexColor(input?.brightWhite ?? defaults.brightWhite),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeTheme(
|
|
284
|
+
input: WebHostTerminalTheme | undefined,
|
|
285
|
+
palette: ResolvedWebHostTerminalPalette,
|
|
286
|
+
defaults: Required<WebHostTerminalTheme>
|
|
287
|
+
): Required<WebHostTerminalTheme> {
|
|
288
|
+
const derived = themeFromPalette(palette, defaults);
|
|
289
|
+
return {
|
|
290
|
+
foreground: normalizeHexColor(input?.foreground ?? derived.foreground),
|
|
291
|
+
background: normalizeHexColor(input?.background ?? derived.background),
|
|
292
|
+
tint: normalizeHexColor(input?.tint ?? derived.tint),
|
|
293
|
+
separator: normalizeHexColor(input?.separator ?? derived.separator),
|
|
294
|
+
selection: normalizeHexColor(input?.selection ?? derived.selection),
|
|
295
|
+
placeholder: normalizeHexColor(input?.placeholder ?? derived.placeholder),
|
|
296
|
+
link: normalizeHexColor(input?.link ?? derived.link),
|
|
297
|
+
fill: normalizeHexColor(input?.fill ?? derived.fill),
|
|
298
|
+
windowBackground: normalizeHexColor(input?.windowBackground ?? derived.windowBackground),
|
|
299
|
+
success: normalizeHexColor(input?.success ?? derived.success),
|
|
300
|
+
warning: normalizeHexColor(input?.warning ?? derived.warning),
|
|
301
|
+
danger: normalizeHexColor(input?.danger ?? derived.danger),
|
|
302
|
+
info: normalizeHexColor(input?.info ?? derived.info),
|
|
303
|
+
muted: normalizeHexColor(input?.muted ?? derived.muted),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function themeFromPalette(
|
|
308
|
+
palette: ResolvedWebHostTerminalPalette,
|
|
309
|
+
defaults: Required<WebHostTerminalTheme>
|
|
310
|
+
): Required<WebHostTerminalTheme> {
|
|
311
|
+
return {
|
|
312
|
+
foreground: palette.foreground,
|
|
313
|
+
background: palette.background,
|
|
314
|
+
tint: palette.cursor,
|
|
315
|
+
separator: palette.ansi.brightBlack,
|
|
316
|
+
selection: palette.selectionBackground,
|
|
317
|
+
placeholder: palette.ansi.brightBlack,
|
|
318
|
+
link: palette.ansi.blue,
|
|
319
|
+
fill: defaults.fill,
|
|
320
|
+
windowBackground: palette.background,
|
|
321
|
+
success: palette.ansi.green,
|
|
322
|
+
warning: palette.ansi.yellow,
|
|
323
|
+
danger: palette.ansi.red,
|
|
324
|
+
info: palette.ansi.cyan,
|
|
325
|
+
muted: palette.ansi.brightBlack,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function paletteToIndexedMap(
|
|
330
|
+
ansi: Required<WebHostANSIColors>
|
|
331
|
+
): Record<string, string> {
|
|
332
|
+
return {
|
|
333
|
+
0: ansi.black,
|
|
334
|
+
1: ansi.red,
|
|
335
|
+
2: ansi.green,
|
|
336
|
+
3: ansi.yellow,
|
|
337
|
+
4: ansi.blue,
|
|
338
|
+
5: ansi.magenta,
|
|
339
|
+
6: ansi.cyan,
|
|
340
|
+
7: ansi.white,
|
|
341
|
+
8: ansi.brightBlack,
|
|
342
|
+
9: ansi.brightRed,
|
|
343
|
+
10: ansi.brightGreen,
|
|
344
|
+
11: ansi.brightYellow,
|
|
345
|
+
12: ansi.brightBlue,
|
|
346
|
+
13: ansi.brightMagenta,
|
|
347
|
+
14: ansi.brightCyan,
|
|
348
|
+
15: ansi.brightWhite,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function normalizeFontSize(fontSize: number): number {
|
|
353
|
+
return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 14;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function normalizeOpacity(opacity: number): number {
|
|
357
|
+
if (!Number.isFinite(opacity)) {
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return Math.min(1, Math.max(0, opacity));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function normalizeHexColor(value: string): string {
|
|
365
|
+
const trimmed = value.trim();
|
|
366
|
+
const normalized = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
367
|
+
if (!/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(normalized)) {
|
|
368
|
+
throw new Error(`Invalid hex color: ${value}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return normalized.toLowerCase();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function hexToRgba(
|
|
375
|
+
color: string,
|
|
376
|
+
opacity: number
|
|
377
|
+
): string {
|
|
378
|
+
const normalized = normalizeHexColor(color);
|
|
379
|
+
const alpha = normalizeOpacity(opacity);
|
|
380
|
+
const channels = parseHexColor(normalized);
|
|
381
|
+
if (!channels) {
|
|
382
|
+
return normalized;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const finalAlpha = Math.round(channels.alpha * alpha * 1000) / 1000;
|
|
386
|
+
return `rgba(${channels.red}, ${channels.green}, ${channels.blue}, ${finalAlpha})`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function parseHexColor(
|
|
390
|
+
color: string
|
|
391
|
+
): {
|
|
392
|
+
red: number;
|
|
393
|
+
green: number;
|
|
394
|
+
blue: number;
|
|
395
|
+
alpha: number;
|
|
396
|
+
} | undefined {
|
|
397
|
+
const hex = color.startsWith("#") ? color.slice(1) : color;
|
|
398
|
+
const normalized = hex.length === 3 || hex.length === 4
|
|
399
|
+
? hex.split("").map((ch) => ch + ch).join("")
|
|
400
|
+
: hex;
|
|
401
|
+
|
|
402
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const red = Number.parseInt(normalized.slice(0, 2), 16);
|
|
407
|
+
const green = Number.parseInt(normalized.slice(2, 4), 16);
|
|
408
|
+
const blue = Number.parseInt(normalized.slice(4, 6), 16);
|
|
409
|
+
const alpha = normalized.length === 8
|
|
410
|
+
? Number.parseInt(normalized.slice(6, 8), 16) / 255
|
|
411
|
+
: 1;
|
|
412
|
+
return { red, green, blue, alpha };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function contrastRatio(
|
|
416
|
+
foreground: string,
|
|
417
|
+
background: string
|
|
418
|
+
): number {
|
|
419
|
+
const fg = relativeLuminance(foreground);
|
|
420
|
+
const bg = relativeLuminance(background);
|
|
421
|
+
const lighter = Math.max(fg, bg);
|
|
422
|
+
const darker = Math.min(fg, bg);
|
|
423
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function relativeLuminance(color: string): number {
|
|
427
|
+
const channels = parseHexColor(normalizeHexColor(color));
|
|
428
|
+
if (!channels) {
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const toLinear = (channel: number) => {
|
|
433
|
+
const value = channel / 255;
|
|
434
|
+
return value <= 0.03928
|
|
435
|
+
? value / 12.92
|
|
436
|
+
: ((value + 0.055) / 1.055) ** 2.4;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return 0.2126 * toLinear(channels.red) + 0.7152 * toLinear(channels.green)
|
|
440
|
+
+ 0.0722 * toLinear(channels.blue);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function encodeBase64(value: string): string {
|
|
444
|
+
if (typeof btoa === "function") {
|
|
445
|
+
const bytes = new TextEncoder().encode(value);
|
|
446
|
+
let binary = "";
|
|
447
|
+
for (const byte of bytes) {
|
|
448
|
+
binary += String.fromCharCode(byte);
|
|
449
|
+
}
|
|
450
|
+
return btoa(binary);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function decodeBase64(value: string): string | undefined {
|
|
457
|
+
try {
|
|
458
|
+
if (typeof atob === "function") {
|
|
459
|
+
const binary = atob(value);
|
|
460
|
+
const bytes = new Uint8Array(binary.length);
|
|
461
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
462
|
+
bytes[index] = binary.charCodeAt(index);
|
|
463
|
+
}
|
|
464
|
+
return new TextDecoder().decode(bytes);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return Buffer.from(value, "base64").toString("utf8");
|
|
468
|
+
} catch {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|