@swifttui/web 0.0.13 → 0.0.15

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.
Files changed (92) hide show
  1. package/README.md +24 -10
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +9 -0
  4. package/dist/manifest.d.ts +2 -0
  5. package/dist/manifest.js +2 -0
  6. package/dist/src/AccessibilityTree.js +156 -0
  7. package/dist/src/AccessibilityTree.js.map +1 -0
  8. package/dist/src/BoxDrawingRenderer.js +1106 -0
  9. package/dist/src/BoxDrawingRenderer.js.map +1 -0
  10. package/dist/src/WebHostApp.d.ts +41 -0
  11. package/dist/src/WebHostApp.js +135 -0
  12. package/dist/src/WebHostApp.js.map +1 -0
  13. package/dist/src/WebHostSceneManifest.d.ts +18 -0
  14. package/dist/src/WebHostSceneManifest.js +70 -0
  15. package/dist/src/WebHostSceneManifest.js.map +1 -0
  16. package/dist/src/WebHostSceneRuntime.d.ts +112 -0
  17. package/dist/src/WebHostSceneRuntime.js +651 -0
  18. package/dist/src/WebHostSceneRuntime.js.map +1 -0
  19. package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
  20. package/dist/src/WebHostSurfaceTransport.js +252 -0
  21. package/dist/src/WebHostSurfaceTransport.js.map +1 -0
  22. package/dist/src/WebHostTerminalStyle.d.ts +92 -0
  23. package/dist/src/WebHostTerminalStyle.js +277 -0
  24. package/dist/src/WebHostTerminalStyle.js.map +1 -0
  25. package/dist/src/WebHostTestFixtures.d.ts +5 -0
  26. package/dist/src/WebHostTestFixtures.js +9 -0
  27. package/dist/src/WebHostTestFixtures.js.map +1 -0
  28. package/dist/src/WebSocketSceneBridge.d.ts +53 -0
  29. package/dist/src/WebSocketSceneBridge.js +124 -0
  30. package/dist/src/WebSocketSceneBridge.js.map +1 -0
  31. package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
  32. package/dist/src/wasi/BrowserWASIBridge.js +97 -0
  33. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
  34. package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
  35. package/dist/src/wasi/SharedInputQueue.js +102 -0
  36. package/dist/src/wasi/SharedInputQueue.js.map +1 -0
  37. package/dist/src/wasi/StdIOPipe.d.ts +15 -0
  38. package/dist/src/wasi/StdIOPipe.js +56 -0
  39. package/dist/src/wasi/StdIOPipe.js.map +1 -0
  40. package/dist/src/wasi/WasiPollScheduler.js +114 -0
  41. package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
  42. package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
  43. package/dist/src/wasi/WasmSceneRuntime.js +119 -0
  44. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
  45. package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
  46. package/dist/src/wasi/WasmSceneWorker.js +109 -0
  47. package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
  48. package/dist/testing.d.ts +2 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/wasi-worker.d.ts +2 -0
  51. package/dist/wasi-worker.js +2 -0
  52. package/dist/wasi.d.ts +6 -0
  53. package/dist/wasi.js +6 -0
  54. package/dist/websocket.d.ts +2 -0
  55. package/dist/websocket.js +2 -0
  56. package/package.json +49 -18
  57. package/AGENTS.md +0 -52
  58. package/cli.ts +0 -168
  59. package/index.html +0 -50
  60. package/index.ts +0 -8
  61. package/manifest.ts +0 -1
  62. package/src/AccessibilityTree.ts +0 -262
  63. package/src/BoxDrawingRenderer.ts +0 -585
  64. package/src/PublicEntrypointBoundary.test.ts +0 -20
  65. package/src/WebHostApp.test.ts +0 -222
  66. package/src/WebHostApp.ts +0 -269
  67. package/src/WebHostSceneManifest.test.ts +0 -38
  68. package/src/WebHostSceneManifest.ts +0 -156
  69. package/src/WebHostSceneRuntime.test.ts +0 -1982
  70. package/src/WebHostSceneRuntime.ts +0 -1142
  71. package/src/WebHostSurfaceTransport.test.ts +0 -362
  72. package/src/WebHostSurfaceTransport.ts +0 -691
  73. package/src/WebHostTerminalStyle.test.ts +0 -123
  74. package/src/WebHostTerminalStyle.ts +0 -471
  75. package/src/WebHostTestFixtures.ts +0 -10
  76. package/src/WebSocketSceneBridge.test.ts +0 -198
  77. package/src/WebSocketSceneBridge.ts +0 -233
  78. package/src/browser.ts +0 -59
  79. package/src/wasi/BrowserWASIBridge.test.ts +0 -168
  80. package/src/wasi/BrowserWASIBridge.ts +0 -167
  81. package/src/wasi/SharedInputQueue.test.ts +0 -146
  82. package/src/wasi/SharedInputQueue.ts +0 -199
  83. package/src/wasi/StdIOPipe.ts +0 -72
  84. package/src/wasi/WasiPollScheduler.test.ts +0 -176
  85. package/src/wasi/WasiPollScheduler.ts +0 -305
  86. package/src/wasi/WasmSceneRuntime.ts +0 -205
  87. package/src/wasi/WasmSceneWorker.ts +0 -182
  88. package/testing.ts +0 -1
  89. package/tsconfig.json +0 -29
  90. package/wasi-worker.ts +0 -1
  91. package/wasi.ts +0 -4
  92. package/websocket.ts +0 -1
@@ -1,198 +0,0 @@
1
- import { expect, test } from "bun:test";
2
-
3
- import {
4
- WebSocketSceneBridge,
5
- webSocketSceneURL,
6
- type WebSocketSceneSocket,
7
- } from "./WebSocketSceneBridge.ts";
8
-
9
- const encoder = new TextEncoder();
10
- const decoder = new TextDecoder();
11
-
12
- class FakeWebSocket implements WebSocketSceneSocket {
13
- binaryType: BinaryType = "blob";
14
- readyState = 0;
15
- readonly sent: Uint8Array[] = [];
16
- closeCode?: number;
17
- closeReason?: string;
18
-
19
- private readonly listeners = new Map<string, Set<(event: unknown) => void>>();
20
-
21
- send(
22
- data: string | ArrayBufferLike | Blob | ArrayBufferView
23
- ): void {
24
- if (typeof data === "string") {
25
- this.sent.push(encoder.encode(data));
26
- } else if (data instanceof Uint8Array) {
27
- this.sent.push(new Uint8Array(data));
28
- } else if (data instanceof ArrayBuffer) {
29
- this.sent.push(new Uint8Array(data));
30
- } else if (ArrayBuffer.isView(data)) {
31
- this.sent.push(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
32
- } else {
33
- throw new Error("fake socket does not support Blob sends");
34
- }
35
- }
36
-
37
- close(
38
- code?: number,
39
- reason?: string
40
- ): void {
41
- this.closeCode = code;
42
- this.closeReason = reason;
43
- this.readyState = 3;
44
- }
45
-
46
- addEventListener(
47
- type: string,
48
- listener: (event: unknown) => void
49
- ): void {
50
- const listeners = this.listeners.get(type) ?? new Set();
51
- listeners.add(listener);
52
- this.listeners.set(type, listeners);
53
- }
54
-
55
- removeEventListener(
56
- type: string,
57
- listener: (event: unknown) => void
58
- ): void {
59
- this.listeners.get(type)?.delete(listener);
60
- }
61
-
62
- open(): void {
63
- this.readyState = 1;
64
- this.emit("open", {});
65
- }
66
-
67
- message(
68
- data: unknown
69
- ): void {
70
- this.emit("message", { data });
71
- }
72
-
73
- private emit(
74
- type: string,
75
- event: unknown
76
- ): void {
77
- for (const listener of this.listeners.get(type) ?? []) {
78
- listener(event);
79
- }
80
- }
81
- }
82
-
83
- test("websocket scene URLs use the embedded host path and token", () => {
84
- expect(String(webSocketSceneURL({
85
- sceneId: "main",
86
- token: "test-token",
87
- baseURL: "http://127.0.0.1:9123/",
88
- }))).toBe("ws://127.0.0.1:9123/ws/scene/main?token=test-token");
89
-
90
- expect(String(webSocketSceneURL({
91
- sceneId: "main",
92
- token: "secure-token",
93
- baseURL: "https://localhost:9443/app/",
94
- }))).toBe("wss://localhost:9443/app/ws/scene/main?token=secure-token");
95
- });
96
-
97
- test("bridge decodes websocket output and sends queued input when the socket opens", async () => {
98
- const socket = new FakeWebSocket();
99
- const bridge = new WebSocketSceneBridge({
100
- sceneId: "main",
101
- token: "test-token",
102
- baseURL: "http://127.0.0.1:9123/",
103
- webSocketFactory: () => socket,
104
- });
105
- const frames: unknown[] = [];
106
- const text: string[] = [];
107
- const clipboard: string[] = [];
108
- const runtimeIssues: unknown[] = [];
109
- const frameDiagnostics: unknown[] = [];
110
-
111
- bridge.bindOutput({
112
- presentSurface: (frame) => frames.push(frame),
113
- writeClipboard: (value) => clipboard.push(value),
114
- notifyRuntimeIssue: (issue) => runtimeIssues.push(issue),
115
- recordFrameDiagnostic: (diagnostic) => frameDiagnostics.push(diagnostic),
116
- writeOutput: (chunk) => text.push(chunk),
117
- });
118
-
119
- bridge.resize(100, 32, 9, 18);
120
- expect(socket.sent).toHaveLength(0);
121
-
122
- socket.open();
123
- expect(decoder.decode(socket.sent[0])).toBe("\u001Eresize:100:32:9:18\n");
124
-
125
- socket.message(encoder.encode(
126
- '\u001Esurface:{"version":2,"width":2,"height":1,"styles":[null],"rows":[[]],'
127
- + '"accessibilityTree":[{"id":"root","rect":[0,0,2,1],"role":"group"}]}\n'
128
- + '\u001Eclipboard:{"text":"copied text"}\n'
129
- + '\u001EruntimeIssue:{"severity":"warning","code":"toolbar.unhostedItems",'
130
- + '"message":"Toolbar item was not rendered",'
131
- + '"description":"SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered"}\n'
132
- + '\u001EframeDiagnostic:{"format":"swift-tui-frame-diagnostics-v1",'
133
- + '"header":["frame","total_ms"],"fields":["7","14.20"]}\n'
134
- + "legacy output\n"
135
- ));
136
- await Promise.resolve();
137
-
138
- expect(frames).toHaveLength(1);
139
- expect(frames[0]).toMatchObject({
140
- version: 2,
141
- width: 2,
142
- accessibilityTree: [{ id: "root", role: "group" }],
143
- });
144
- expect(clipboard).toEqual(["copied text"]);
145
- expect(runtimeIssues).toEqual([
146
- {
147
- severity: "warning",
148
- code: "toolbar.unhostedItems",
149
- message: "Toolbar item was not rendered",
150
- description: "SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered",
151
- },
152
- ]);
153
- expect(frameDiagnostics).toEqual([
154
- {
155
- format: "swift-tui-frame-diagnostics-v1",
156
- header: ["frame", "total_ms"],
157
- fields: ["7", "14.20"],
158
- },
159
- ]);
160
- expect(text).toEqual(["legacy output\n"]);
161
-
162
- bridge.sendInput(encoder.encode("\u001Ekey:return:0\n"));
163
- expect(decoder.decode(socket.sent.at(-1))).toBe("\u001Ekey:return:0\n");
164
-
165
- bridge.dispose();
166
- expect(socket.closeCode).toBe(1000);
167
- expect(socket.closeReason).toBe("WebHost scene disposed");
168
- });
169
-
170
- test("bridge buffers output until a runtime binds a sink", async () => {
171
- const socket = new FakeWebSocket();
172
- const bridge = new WebSocketSceneBridge({
173
- sceneId: "main",
174
- token: "test-token",
175
- baseURL: "http://127.0.0.1:9123/",
176
- webSocketFactory: () => socket,
177
- });
178
- const frames: unknown[] = [];
179
-
180
- socket.message(encoder.encode(
181
- '\u001Esurface:{"version":1,"width":3,"height":1,"styles":[null],"rows":[[]]}\n'
182
- ));
183
- await Promise.resolve();
184
-
185
- bridge.bindOutput({
186
- presentSurface: (frame) => frames.push(frame),
187
- });
188
-
189
- expect(frames).toEqual([
190
- {
191
- version: 1,
192
- width: 3,
193
- height: 1,
194
- styles: [null],
195
- rows: [[]],
196
- },
197
- ]);
198
- });
@@ -1,233 +0,0 @@
1
- import {
2
- WebHostOutputDecoder,
3
- encodeRenderStyleControlMessage,
4
- encodeResizeControlMessage,
5
- type WebHostOutputRecord,
6
- type WebHostOutputSink,
7
- } from "./WebHostSurfaceTransport.ts";
8
- import type { WebHostTerminalStyle } from "./WebHostTerminalStyle.ts";
9
- import type { WebHostSceneBridge } from "./WebHostSceneRuntime.ts";
10
-
11
- export interface WebSocketSceneBridgeOptions {
12
- sceneId: string;
13
- token: string;
14
- baseURL?: string | URL;
15
- webSocketURL?: string | URL;
16
- webSocketFactory?: WebSocketSceneBridgeFactory;
17
- }
18
-
19
- export type WebSocketSceneBridgeFactory = (url: string | URL) => WebSocketSceneSocket;
20
-
21
- export interface WebSocketSceneSocket {
22
- binaryType: BinaryType;
23
- readonly readyState: number;
24
- send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
25
- close(code?: number, reason?: string): void;
26
- addEventListener(type: "open", listener: (event: Event) => void): void;
27
- addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
28
- addEventListener(type: "close", listener: (event: CloseEvent) => void): void;
29
- addEventListener(type: "error", listener: (event: Event) => void): void;
30
- removeEventListener(type: "open", listener: (event: Event) => void): void;
31
- removeEventListener(type: "message", listener: (event: MessageEvent) => void): void;
32
- removeEventListener(type: "close", listener: (event: CloseEvent) => void): void;
33
- removeEventListener(type: "error", listener: (event: Event) => void): void;
34
- }
35
-
36
- const socketOpenState = 1;
37
- const textEncoder = new TextEncoder();
38
-
39
- export class WebSocketSceneBridge implements WebHostSceneBridge {
40
- readonly url: URL;
41
-
42
- private readonly socket: WebSocketSceneSocket;
43
- private readonly decoder = new WebHostOutputDecoder();
44
- private readonly queuedInput: Uint8Array[] = [];
45
- private readonly queuedOutput: WebHostOutputRecord[] = [];
46
- private sink?: WebHostOutputSink;
47
- private disposed = false;
48
-
49
- private readonly handleOpen = () => {
50
- this.flushQueuedInput();
51
- };
52
-
53
- private readonly handleMessage = (event: MessageEvent) => {
54
- void this.receive(event.data);
55
- };
56
-
57
- private readonly handleClose = () => {
58
- for (const record of this.decoder.flush()) {
59
- this.deliver(record);
60
- }
61
- };
62
-
63
- private readonly handleError = () => {};
64
-
65
- constructor(options: WebSocketSceneBridgeOptions) {
66
- this.url = webSocketSceneURL(options);
67
- this.socket = (options.webSocketFactory ?? defaultWebSocketFactory)(this.url);
68
- this.socket.binaryType = "arraybuffer";
69
- this.socket.addEventListener("open", this.handleOpen);
70
- this.socket.addEventListener("message", this.handleMessage);
71
- this.socket.addEventListener("close", this.handleClose);
72
- this.socket.addEventListener("error", this.handleError);
73
- }
74
-
75
- bindOutput(
76
- sink: WebHostOutputSink
77
- ): void {
78
- this.sink = sink;
79
- while (this.queuedOutput.length > 0) {
80
- this.deliver(this.queuedOutput.shift()!);
81
- }
82
- }
83
-
84
- resize(
85
- columns: number,
86
- rows: number,
87
- cellWidth?: number,
88
- cellHeight?: number
89
- ): void {
90
- this.sendInput(encodeResizeControlMessage(columns, rows, cellWidth, cellHeight));
91
- }
92
-
93
- updateRenderStyle(
94
- style: WebHostTerminalStyle
95
- ): void {
96
- this.sendInput(encodeRenderStyleControlMessage(style));
97
- }
98
-
99
- sendInput(
100
- chunk: Uint8Array
101
- ): void {
102
- if (this.disposed) {
103
- return;
104
- }
105
-
106
- const copy = new Uint8Array(chunk);
107
- if (this.socket.readyState === socketOpenState) {
108
- this.socket.send(copy);
109
- } else {
110
- this.queuedInput.push(copy);
111
- }
112
- }
113
-
114
- dispose(): void {
115
- if (this.disposed) {
116
- return;
117
- }
118
- this.disposed = true;
119
- this.socket.removeEventListener("open", this.handleOpen);
120
- this.socket.removeEventListener("message", this.handleMessage);
121
- this.socket.removeEventListener("close", this.handleClose);
122
- this.socket.removeEventListener("error", this.handleError);
123
- this.queuedInput.length = 0;
124
- this.queuedOutput.length = 0;
125
- this.socket.close(1000, "WebHost scene disposed");
126
- }
127
-
128
- private async receive(
129
- message: unknown
130
- ): Promise<void> {
131
- if (this.disposed) {
132
- return;
133
- }
134
-
135
- const bytes = await bytesFromWebSocketMessage(message);
136
- if (!bytes) {
137
- return;
138
- }
139
-
140
- for (const record of this.decoder.feed(bytes)) {
141
- this.deliver(record);
142
- }
143
- }
144
-
145
- private deliver(
146
- record: WebHostOutputRecord
147
- ): void {
148
- const sink = this.sink;
149
- if (!sink) {
150
- this.queuedOutput.push(record);
151
- return;
152
- }
153
-
154
- switch (record.type) {
155
- case "surface":
156
- sink.presentSurface(record.frame);
157
- break;
158
- case "clipboard":
159
- void sink.writeClipboard?.(record.text);
160
- break;
161
- case "runtimeIssue":
162
- sink.notifyRuntimeIssue?.(record.issue);
163
- break;
164
- case "frameDiagnostic":
165
- sink.recordFrameDiagnostic?.(record.diagnostic);
166
- break;
167
- case "text":
168
- sink.writeOutput?.(record.text);
169
- break;
170
- }
171
- }
172
-
173
- private flushQueuedInput(): void {
174
- if (this.disposed || this.socket.readyState !== socketOpenState) {
175
- return;
176
- }
177
- while (this.queuedInput.length > 0) {
178
- this.socket.send(this.queuedInput.shift()!);
179
- }
180
- }
181
- }
182
-
183
- export function webSocketSceneURL(
184
- options: Pick<WebSocketSceneBridgeOptions, "baseURL" | "webSocketURL" | "sceneId" | "token">
185
- ): URL {
186
- if (options.webSocketURL) {
187
- const explicit = new URL(String(options.webSocketURL), currentPageURL());
188
- explicit.searchParams.set("token", options.token);
189
- return explicit;
190
- }
191
-
192
- const url = new URL(String(options.baseURL ?? currentPageURL()), currentPageURL());
193
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
194
- const basePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
195
- url.pathname = `${basePath}/ws/scene/${encodeURIComponent(options.sceneId)}`;
196
- url.search = "";
197
- url.searchParams.set("token", options.token);
198
- return url;
199
- }
200
-
201
- async function bytesFromWebSocketMessage(
202
- message: unknown
203
- ): Promise<Uint8Array | undefined> {
204
- if (typeof message === "string") {
205
- return textEncoder.encode(message);
206
- }
207
- if (message instanceof Uint8Array) {
208
- return message;
209
- }
210
- if (message instanceof ArrayBuffer) {
211
- return new Uint8Array(message);
212
- }
213
- if (ArrayBuffer.isView(message)) {
214
- return new Uint8Array(message.buffer, message.byteOffset, message.byteLength);
215
- }
216
- if (typeof Blob !== "undefined" && message instanceof Blob) {
217
- return new Uint8Array(await message.arrayBuffer());
218
- }
219
- return undefined;
220
- }
221
-
222
- function defaultWebSocketFactory(
223
- url: string | URL
224
- ): WebSocketSceneSocket {
225
- if (typeof WebSocket === "undefined") {
226
- throw new Error("WebSocket is not available");
227
- }
228
- return new WebSocket(url) as WebSocketSceneSocket;
229
- }
230
-
231
- function currentPageURL(): string {
232
- return globalThis.location?.href ?? "http://127.0.0.1/";
233
- }
package/src/browser.ts DELETED
@@ -1,59 +0,0 @@
1
- import { createWebHostApp, type WebHostTerminalStyle } from "./WebHostApp.ts";
2
-
3
- declare global {
4
- interface Window {
5
- __WEBTUI__?: {
6
- manifestUrl?: string;
7
- initialSceneId?: string;
8
- style?: WebHostTerminalStyle;
9
- embeddedHost?: {
10
- token?: string;
11
- webSocketBaseURL?: string;
12
- };
13
- };
14
- __WEBTUI_APP__?: Awaited<ReturnType<typeof createWebHostApp>>;
15
- }
16
- }
17
-
18
- async function bootstrap(): Promise<void> {
19
- const mount = document.getElementById("webhost-root");
20
- if (!mount) {
21
- throw new Error("webhost root element not found");
22
- }
23
-
24
- const config = window.__WEBTUI__ ?? {};
25
- const pageURL = new URL(globalThis.location?.href ?? import.meta.url);
26
- const embeddedToken = config.embeddedHost?.token ?? pageURL.searchParams.get("token") ?? undefined;
27
- const manifestUrl = tokenizedURL(
28
- config.manifestUrl ?? new URL("./scene-manifest.json", pageURL),
29
- embeddedToken
30
- );
31
- const controller = await createWebHostApp({
32
- mount,
33
- manifestUrl,
34
- initialSceneId: config.initialSceneId,
35
- style: config.style,
36
- embeddedHost: embeddedToken
37
- ? {
38
- token: embeddedToken,
39
- webSocketBaseURL: config.embeddedHost?.webSocketBaseURL ?? new URL("./", pageURL).href,
40
- }
41
- : undefined,
42
- });
43
-
44
- window.__WEBTUI_APP__ = controller;
45
- }
46
-
47
- void bootstrap();
48
-
49
- function tokenizedURL(
50
- value: string | URL,
51
- token: string | undefined
52
- ): string | URL {
53
- if (!token) {
54
- return value;
55
- }
56
- const url = new URL(String(value), globalThis.location?.href ?? import.meta.url);
57
- url.searchParams.set("token", token);
58
- return url;
59
- }
@@ -1,168 +0,0 @@
1
- import { expect, test } from "bun:test";
2
-
3
- import {
4
- BrowserWASIBridge,
5
- encodeRenderStyleControlMessage,
6
- encodeResizeControlMessage,
7
- } from "./BrowserWASIBridge.ts";
8
- import {
9
- decodeWebHostTerminalRenderStyleBase64,
10
- encodeWebHostTerminalRenderStyleBase64,
11
- } from "../WebHostTerminalStyle.ts";
12
-
13
- test("bridge seeds initial render style and emits runtime style updates", async () => {
14
- const style = {
15
- theme: {
16
- foreground: "#ededed",
17
- background: "#111111",
18
- tint: "#56b6c2",
19
- separator: "#4c566a",
20
- selection: "#2e3440",
21
- placeholder: "#8c92ac",
22
- link: "#5ba3ff",
23
- fill: "#2b303b",
24
- windowBackground: "#15181e",
25
- success: "#61c67b",
26
- warning: "#ebb33c",
27
- danger: "#e05757",
28
- info: "#56b6c2",
29
- muted: "#8c92ac",
30
- },
31
- };
32
-
33
- const bridge = new BrowserWASIBridge({
34
- sceneId: "main",
35
- columns: 80,
36
- rows: 24,
37
- renderStyle: style,
38
- });
39
-
40
- expect(
41
- decodeWebHostTerminalRenderStyleBase64(bridge.environment.TUIGUI_RENDER_STYLE ?? "")
42
- ?.appearance.backgroundColor
43
- ).toBe("#111111");
44
- expect(
45
- bridge.environment.TUIGUI_RENDER_STYLE
46
- ).toBe(encodeWebHostTerminalRenderStyleBase64(style));
47
- expect(bridge.environment.TUIGUI_TRANSPORT).toBe("surface");
48
- expect(bridge.environment.TUIGUI_SURFACE_DELTA).toBe("1");
49
-
50
- bridge.updateRenderStyle(style);
51
- const input = await bridge.stdin.read();
52
- expect(Array.from(input ?? [])).toEqual(
53
- Array.from(encodeRenderStyleControlMessage(style))
54
- );
55
- });
56
-
57
- test("bridge allows callers to disable surface delta support", () => {
58
- const bridge = new BrowserWASIBridge({
59
- sceneId: "main",
60
- columns: 80,
61
- rows: 24,
62
- environment: {
63
- TUIGUI_SURFACE_DELTA: "0",
64
- },
65
- });
66
-
67
- expect(bridge.environment.TUIGUI_SURFACE_DELTA).toBe("0");
68
- });
69
-
70
- test("bridge resize updates environment, emits control input, and notifies listeners", async () => {
71
- const bridge = new BrowserWASIBridge({
72
- sceneId: "main",
73
- columns: 80,
74
- rows: 24,
75
- });
76
- const seen: Array<[number, number, number | undefined, number | undefined]> = [];
77
- const unsubscribe = bridge.subscribeResize((columns, rows, cellWidth, cellHeight) => {
78
- seen.push([columns, rows, cellWidth, cellHeight]);
79
- });
80
-
81
- expect(seen).toEqual([[80, 24, undefined, undefined]]);
82
-
83
- bridge.resize(132, 41, 9, 18);
84
-
85
- expect(bridge.environment.TUIGUI_COLUMNS).toBe("132");
86
- expect(bridge.environment.TUIGUI_ROWS).toBe("41");
87
- expect(seen).toEqual([
88
- [80, 24, undefined, undefined],
89
- [132, 41, 9, 18],
90
- ]);
91
-
92
- const input = await bridge.stdin.read();
93
- expect(Array.from(input ?? [])).toEqual(Array.from(encodeResizeControlMessage(132, 41, 9, 18)));
94
-
95
- unsubscribe();
96
- bridge.resize(90, 30);
97
- expect(seen).toEqual([
98
- [80, 24, undefined, undefined],
99
- [132, 41, 9, 18],
100
- ]);
101
-
102
- const replayed: Array<[number, number, number | undefined, number | undefined]> = [];
103
- bridge.subscribeResize((columns, rows, cellWidth, cellHeight) => {
104
- replayed.push([columns, rows, cellWidth, cellHeight]);
105
- })();
106
- expect(replayed).toEqual([[90, 30, undefined, undefined]]);
107
- });
108
-
109
- test("bridge delivers typed clipboard output to sinks", () => {
110
- const bridge = new BrowserWASIBridge({
111
- sceneId: "main",
112
- columns: 80,
113
- rows: 24,
114
- });
115
- const clipboard: string[] = [];
116
-
117
- bridge.bindOutput({
118
- presentSurface: () => {},
119
- writeClipboard: (text) => clipboard.push(text),
120
- });
121
-
122
- bridge.stdout.write(new TextEncoder().encode('\u001Eclipboard:{"text":"copied text"}\n'));
123
-
124
- expect(clipboard).toEqual(["copied text"]);
125
- });
126
-
127
- test("bridge delivers typed runtime issues and frame diagnostics to sinks", () => {
128
- const bridge = new BrowserWASIBridge({
129
- sceneId: "main",
130
- columns: 80,
131
- rows: 24,
132
- });
133
- const runtimeIssues: unknown[] = [];
134
- const frameDiagnostics: unknown[] = [];
135
- const text: string[] = [];
136
-
137
- bridge.bindOutput({
138
- presentSurface: () => {},
139
- notifyRuntimeIssue: (issue) => runtimeIssues.push(issue),
140
- recordFrameDiagnostic: (diagnostic) => frameDiagnostics.push(diagnostic),
141
- writeOutput: (chunk) => text.push(chunk),
142
- });
143
-
144
- bridge.stdout.write(new TextEncoder().encode(
145
- '\u001EruntimeIssue:{"severity":"warning","code":"toolbar.unhostedItems",'
146
- + '"message":"Toolbar item was not rendered",'
147
- + '"description":"SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered"}\n'
148
- + '\u001EframeDiagnostic:{"format":"swift-tui-frame-diagnostics-v1",'
149
- + '"header":["frame","total_ms"],"fields":["7","14.20"]}\n'
150
- ));
151
-
152
- expect(runtimeIssues).toEqual([
153
- {
154
- severity: "warning",
155
- code: "toolbar.unhostedItems",
156
- message: "Toolbar item was not rendered",
157
- description: "SwiftTUI runtime warning [toolbar.unhostedItems] Toolbar item was not rendered",
158
- },
159
- ]);
160
- expect(frameDiagnostics).toEqual([
161
- {
162
- format: "swift-tui-frame-diagnostics-v1",
163
- header: ["frame", "total_ms"],
164
- fields: ["7", "14.20"],
165
- },
166
- ]);
167
- expect(text).toEqual([]);
168
- });