bunite-core 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/host/core/App.ts +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { BrowserView } from "./BrowserView";
|
|
2
2
|
import {
|
|
3
3
|
trackSurface, untrackSurface, getOwnedSurface,
|
|
4
|
-
getHostSurfaceIds, getSurfaceRecord,
|
|
4
|
+
getHostSurfaceIds, getSurfaceRecord, onSurfaceDispose,
|
|
5
5
|
MAX_SURFACES_PER_HOST
|
|
6
6
|
} from "./SurfaceRegistry";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
SurfaceCap, type ImplOf, IpcError,
|
|
9
|
+
type SurfaceEvent, type DialogEvent, type ConsoleEntry,
|
|
10
|
+
} from "../../rpc/index";
|
|
8
11
|
import { Stream } from "../../rpc/stream";
|
|
9
12
|
|
|
10
13
|
function applyHostOffset(hostView: BrowserView, x: number, y: number) {
|
|
@@ -18,22 +21,131 @@ export function onSurfaceInit(cb: SurfaceInitCallback) {
|
|
|
18
21
|
initCallbacks.push(cb);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
type
|
|
22
|
-
const
|
|
24
|
+
type SurfaceEventEmit = (event: { surfaceId: number; event: SurfaceEvent }) => void;
|
|
25
|
+
const surfaceEventSubs = new Map<number, Set<SurfaceEventEmit>>();
|
|
23
26
|
|
|
24
|
-
export function
|
|
25
|
-
const subs =
|
|
27
|
+
export function emitSurfaceEvent(hostViewId: number, surfaceId: number, event: SurfaceEvent) {
|
|
28
|
+
const subs = surfaceEventSubs.get(hostViewId);
|
|
26
29
|
if (!subs) return;
|
|
27
|
-
for (const emit of subs) emit({ surfaceId,
|
|
30
|
+
for (const emit of subs) emit({ surfaceId, event });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type DialogEmit = (event: { surfaceId: number; event: DialogEvent }) => void;
|
|
34
|
+
const dialogSubs = new Map<number, Set<DialogEmit>>();
|
|
35
|
+
|
|
36
|
+
type ConsoleEmit = (event: { surfaceId: number; entry: ConsoleEntry }) => void;
|
|
37
|
+
const consoleSubs = new Map<number, Set<ConsoleEmit>>();
|
|
38
|
+
|
|
39
|
+
const CONSOLE_BUFFER_LIMIT = 200;
|
|
40
|
+
const DEFAULT_DIALOG_TIMEOUT_MS = 5000;
|
|
41
|
+
|
|
42
|
+
type PendingDialog = {
|
|
43
|
+
requestId: number;
|
|
44
|
+
originalKind: "alert" | "confirm" | "prompt" | "beforeunload";
|
|
45
|
+
message: string;
|
|
46
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type SurfaceState = {
|
|
50
|
+
consoleBuffer: ConsoleEntry[];
|
|
51
|
+
dialogTimeoutMs: number | null; // null = no auto-dismiss
|
|
52
|
+
pendingDialogs: Map<number, PendingDialog>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const surfaceState = new Map<number, SurfaceState>();
|
|
56
|
+
|
|
57
|
+
function getOrCreateState(surfaceId: number): SurfaceState {
|
|
58
|
+
let s = surfaceState.get(surfaceId);
|
|
59
|
+
if (!s) {
|
|
60
|
+
s = {
|
|
61
|
+
consoleBuffer: [],
|
|
62
|
+
dialogTimeoutMs: DEFAULT_DIALOG_TIMEOUT_MS,
|
|
63
|
+
pendingDialogs: new Map(),
|
|
64
|
+
};
|
|
65
|
+
surfaceState.set(surfaceId, s);
|
|
66
|
+
}
|
|
67
|
+
return s;
|
|
28
68
|
}
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
const
|
|
70
|
+
export function disposeSurfaceState(surfaceId: number) {
|
|
71
|
+
const s = surfaceState.get(surfaceId);
|
|
72
|
+
if (!s) return;
|
|
73
|
+
for (const p of s.pendingDialogs.values()) {
|
|
74
|
+
if (p.timer) clearTimeout(p.timer);
|
|
75
|
+
}
|
|
76
|
+
surfaceState.delete(surfaceId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wire dispose to any untrack path (remove + removeSurfacesForHostView).
|
|
80
|
+
onSurfaceDispose(disposeSurfaceState);
|
|
81
|
+
|
|
82
|
+
export function clearConsoleBuffer(surfaceId: number) {
|
|
83
|
+
const s = surfaceState.get(surfaceId);
|
|
84
|
+
if (s) s.consoleBuffer.length = 0;
|
|
85
|
+
}
|
|
32
86
|
|
|
33
|
-
export function
|
|
34
|
-
const subs =
|
|
87
|
+
export function emitDialog(hostViewId: number, surfaceId: number, event: DialogEvent) {
|
|
88
|
+
const subs = dialogSubs.get(hostViewId);
|
|
35
89
|
if (!subs) return;
|
|
36
|
-
for (const emit of subs) emit({ surfaceId,
|
|
90
|
+
for (const emit of subs) emit({ surfaceId, event });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function emitConsole(hostViewId: number, surfaceId: number, entry: ConsoleEntry) {
|
|
94
|
+
const state = getOrCreateState(surfaceId);
|
|
95
|
+
state.consoleBuffer.push(entry);
|
|
96
|
+
if (state.consoleBuffer.length > CONSOLE_BUFFER_LIMIT) {
|
|
97
|
+
state.consoleBuffer.splice(0, state.consoleBuffer.length - CONSOLE_BUFFER_LIMIT);
|
|
98
|
+
}
|
|
99
|
+
const subs = consoleSubs.get(hostViewId);
|
|
100
|
+
if (!subs) return;
|
|
101
|
+
for (const emit of subs) emit({ surfaceId, entry });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Called by SurfaceBrowserIPC on native `dialog` event. Stashes pending entry
|
|
105
|
+
* + arms the auto-dismiss timer, then broadcasts to subscribers. */
|
|
106
|
+
export function registerDialogRequest(
|
|
107
|
+
hostViewId: number,
|
|
108
|
+
surfaceId: number,
|
|
109
|
+
request: { requestId: number; kind: "alert" | "confirm" | "prompt" | "beforeunload"; message: string; defaultPrompt?: string }
|
|
110
|
+
) {
|
|
111
|
+
const state = getOrCreateState(surfaceId);
|
|
112
|
+
const view = getSurfaceRecord(surfaceId)?.view;
|
|
113
|
+
|
|
114
|
+
const entry: PendingDialog = {
|
|
115
|
+
requestId: request.requestId,
|
|
116
|
+
originalKind: request.kind,
|
|
117
|
+
message: request.message,
|
|
118
|
+
timer: null,
|
|
119
|
+
};
|
|
120
|
+
if (state.dialogTimeoutMs !== null && view) {
|
|
121
|
+
entry.timer = setTimeout(() => {
|
|
122
|
+
if (!state.pendingDialogs.delete(request.requestId)) return;
|
|
123
|
+
view.respondToDialog(request.requestId, false);
|
|
124
|
+
emitDialog(hostViewId, surfaceId, {
|
|
125
|
+
kind: "auto-dismissed",
|
|
126
|
+
originalKind: entry.originalKind,
|
|
127
|
+
message: entry.message,
|
|
128
|
+
});
|
|
129
|
+
}, state.dialogTimeoutMs);
|
|
130
|
+
}
|
|
131
|
+
state.pendingDialogs.set(request.requestId, entry);
|
|
132
|
+
|
|
133
|
+
emitDialog(hostViewId, surfaceId, {
|
|
134
|
+
kind: request.kind,
|
|
135
|
+
requestId: request.requestId,
|
|
136
|
+
message: request.message,
|
|
137
|
+
defaultPrompt: request.defaultPrompt,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function consumePendingDialog(surfaceId: number, requestId: number): boolean {
|
|
142
|
+
const state = surfaceState.get(surfaceId);
|
|
143
|
+
if (!state) return false;
|
|
144
|
+
const entry = state.pendingDialogs.get(requestId);
|
|
145
|
+
if (!entry) return false;
|
|
146
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
147
|
+
state.pendingDialogs.delete(requestId);
|
|
148
|
+
return true;
|
|
37
149
|
}
|
|
38
150
|
|
|
39
151
|
export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceCap> {
|
|
@@ -87,6 +199,7 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
|
|
|
87
199
|
const record = ownedSurface(surfaceId);
|
|
88
200
|
if (!record) return;
|
|
89
201
|
untrackSurface(surfaceId);
|
|
202
|
+
disposeSurfaceState(surfaceId);
|
|
90
203
|
record.view.remove();
|
|
91
204
|
},
|
|
92
205
|
|
|
@@ -151,45 +264,164 @@ export function createSurfaceCapImpl(hostViewId: number): ImplOf<typeof SurfaceC
|
|
|
151
264
|
return record.view.evaluate(script);
|
|
152
265
|
},
|
|
153
266
|
|
|
267
|
+
click: ({ surfaceId, ...args }) => {
|
|
268
|
+
const record = ownedSurface(surfaceId);
|
|
269
|
+
if (!record) return;
|
|
270
|
+
record.view.click(args);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
type: ({ surfaceId, text }) => {
|
|
274
|
+
const record = ownedSurface(surfaceId);
|
|
275
|
+
if (!record) return;
|
|
276
|
+
record.view.type(text);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
press: ({ surfaceId, key, modifiers, action }) => {
|
|
280
|
+
const record = ownedSurface(surfaceId);
|
|
281
|
+
if (!record) return;
|
|
282
|
+
record.view.press(key, modifiers, action);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
scroll: ({ surfaceId, ...args }) => {
|
|
286
|
+
const record = ownedSurface(surfaceId);
|
|
287
|
+
if (!record) return;
|
|
288
|
+
record.view.scroll(args);
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
mouse: ({ surfaceId, ...args }) => {
|
|
292
|
+
const record = ownedSurface(surfaceId);
|
|
293
|
+
if (!record) return;
|
|
294
|
+
record.view.mouse(args);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
screenshot: async ({ surfaceId, format = "png", quality = 90 }) => {
|
|
298
|
+
const record = ownedSurface(surfaceId);
|
|
299
|
+
if (!record) return { ok: false as const, code: "not_supported" as const, message: "surface not found" };
|
|
300
|
+
return record.view.screenshot(format, quality);
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
waitForSelector: async ({ surfaceId, selector, timeoutMs = 5000 }) => {
|
|
304
|
+
const record = ownedSurface(surfaceId);
|
|
305
|
+
if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
|
|
306
|
+
const deadline = Date.now() + timeoutMs;
|
|
307
|
+
const expr = `!!document.querySelector(${JSON.stringify(selector)})`;
|
|
308
|
+
while (Date.now() < deadline) {
|
|
309
|
+
const res = await record.view.evaluate(expr);
|
|
310
|
+
if (res.ok && res.value === true) return { ok: true as const };
|
|
311
|
+
if (!res.ok && res.code !== "timeout") {
|
|
312
|
+
// Propagate cross_origin distinct from runtime_error — consumer
|
|
313
|
+
// may want to retry with a same-origin sub-surface vs. fix the script.
|
|
314
|
+
const code = res.code === "cross_origin" ? "cross_origin" as const : "runtime_error" as const;
|
|
315
|
+
return { ok: false as const, code, message: res.message };
|
|
316
|
+
}
|
|
317
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
318
|
+
}
|
|
319
|
+
return { ok: false as const, code: "timeout" as const, message: `selector ${JSON.stringify(selector)} not found within ${timeoutMs}ms` };
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
waitForFunction: async ({ surfaceId, expression, timeoutMs = 5000, pollIntervalMs = 50 }) => {
|
|
323
|
+
const record = ownedSurface(surfaceId);
|
|
324
|
+
if (!record) return { ok: false as const, code: "runtime_error" as const, message: "surface not found" };
|
|
325
|
+
const deadline = Date.now() + timeoutMs;
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
const res = await record.view.evaluate(expression);
|
|
328
|
+
if (res.ok && res.value) return { ok: true as const };
|
|
329
|
+
if (!res.ok && res.code !== "timeout") {
|
|
330
|
+
const code = res.code === "cross_origin" ? "cross_origin" as const : "runtime_error" as const;
|
|
331
|
+
return { ok: false as const, code, message: res.message };
|
|
332
|
+
}
|
|
333
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
334
|
+
}
|
|
335
|
+
return { ok: false as const, code: "timeout" as const, message: `function did not satisfy within ${timeoutMs}ms` };
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
respondToDialog: ({ surfaceId, requestId, accept, text }) => {
|
|
339
|
+
const record = ownedSurface(surfaceId);
|
|
340
|
+
if (!record) return;
|
|
341
|
+
if (!consumePendingDialog(surfaceId, requestId)) return; // stale or already auto-dismissed
|
|
342
|
+
record.view.respondToDialog(requestId, accept, text);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
setDialogTimeout: ({ surfaceId, ms }) => {
|
|
346
|
+
const record = ownedSurface(surfaceId);
|
|
347
|
+
if (!record) return;
|
|
348
|
+
const state = getOrCreateState(surfaceId);
|
|
349
|
+
state.dialogTimeoutMs = ms;
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
getConsoleBuffer: ({ surfaceId, clear }) => {
|
|
353
|
+
const record = ownedSurface(surfaceId);
|
|
354
|
+
if (!record) return [];
|
|
355
|
+
const state = getOrCreateState(surfaceId);
|
|
356
|
+
const snapshot = state.consoleBuffer.slice();
|
|
357
|
+
if (clear) state.consoleBuffer.length = 0;
|
|
358
|
+
return snapshot;
|
|
359
|
+
},
|
|
360
|
+
|
|
154
361
|
capabilities: ({ surfaceId }) => {
|
|
155
362
|
const record = ownedSurface(surfaceId);
|
|
156
363
|
if (!record) {
|
|
157
364
|
return {
|
|
158
|
-
evaluate: false, crossOriginEval: false,
|
|
365
|
+
evaluate: false, crossOriginEval: false, surfaceEvents: false,
|
|
159
366
|
nativeInputTrusted: false, click: false, type: false, press: false,
|
|
160
|
-
scroll: false,
|
|
367
|
+
scroll: false, mouse: false, dialogs: false, console: false,
|
|
368
|
+
screenshot: false,
|
|
161
369
|
};
|
|
162
370
|
}
|
|
163
371
|
return record.view.capabilities();
|
|
164
372
|
},
|
|
165
373
|
|
|
166
|
-
|
|
167
|
-
let subs =
|
|
374
|
+
surfaceEvents: ({ surfaceId: filterId }) => Stream.from<SurfaceEvent>((emit, signal) => {
|
|
375
|
+
let subs = surfaceEventSubs.get(hostViewId);
|
|
376
|
+
if (!subs) {
|
|
377
|
+
subs = new Set();
|
|
378
|
+
surfaceEventSubs.set(hostViewId, subs);
|
|
379
|
+
}
|
|
380
|
+
const wrapped: SurfaceEventEmit = ({ surfaceId, event }) => {
|
|
381
|
+
if (surfaceId === filterId) emit(event);
|
|
382
|
+
};
|
|
383
|
+
subs.add(wrapped);
|
|
384
|
+
signal.addEventListener("abort", () => {
|
|
385
|
+
const set = surfaceEventSubs.get(hostViewId);
|
|
386
|
+
if (!set) return;
|
|
387
|
+
set.delete(wrapped);
|
|
388
|
+
if (set.size === 0) surfaceEventSubs.delete(hostViewId);
|
|
389
|
+
});
|
|
390
|
+
}),
|
|
391
|
+
|
|
392
|
+
dialogs: ({ surfaceId: filterId }) => Stream.from<DialogEvent>((emit, signal) => {
|
|
393
|
+
let subs = dialogSubs.get(hostViewId);
|
|
168
394
|
if (!subs) {
|
|
169
395
|
subs = new Set();
|
|
170
|
-
|
|
396
|
+
dialogSubs.set(hostViewId, subs);
|
|
171
397
|
}
|
|
172
|
-
|
|
398
|
+
const wrapped: DialogEmit = ({ surfaceId, event }) => {
|
|
399
|
+
if (surfaceId === filterId) emit(event);
|
|
400
|
+
};
|
|
401
|
+
subs.add(wrapped);
|
|
173
402
|
signal.addEventListener("abort", () => {
|
|
174
|
-
const set =
|
|
403
|
+
const set = dialogSubs.get(hostViewId);
|
|
175
404
|
if (!set) return;
|
|
176
|
-
set.delete(
|
|
177
|
-
if (set.size === 0)
|
|
405
|
+
set.delete(wrapped);
|
|
406
|
+
if (set.size === 0) dialogSubs.delete(hostViewId);
|
|
178
407
|
});
|
|
179
408
|
}),
|
|
180
409
|
|
|
181
|
-
|
|
182
|
-
let subs =
|
|
410
|
+
consoleEvents: ({ surfaceId: filterId }) => Stream.from<ConsoleEntry>((emit, signal) => {
|
|
411
|
+
let subs = consoleSubs.get(hostViewId);
|
|
183
412
|
if (!subs) {
|
|
184
413
|
subs = new Set();
|
|
185
|
-
|
|
414
|
+
consoleSubs.set(hostViewId, subs);
|
|
186
415
|
}
|
|
187
|
-
|
|
416
|
+
const wrapped: ConsoleEmit = ({ surfaceId, entry }) => {
|
|
417
|
+
if (surfaceId === filterId) emit(entry);
|
|
418
|
+
};
|
|
419
|
+
subs.add(wrapped);
|
|
188
420
|
signal.addEventListener("abort", () => {
|
|
189
|
-
const set =
|
|
421
|
+
const set = consoleSubs.get(hostViewId);
|
|
190
422
|
if (!set) return;
|
|
191
|
-
set.delete(
|
|
192
|
-
if (set.size === 0)
|
|
423
|
+
set.delete(wrapped);
|
|
424
|
+
if (set.size === 0) consoleSubs.delete(hostViewId);
|
|
193
425
|
});
|
|
194
426
|
}),
|
|
195
427
|
};
|
|
@@ -21,6 +21,13 @@ export function trackSurface(surfaceId: number, record: SurfaceRecord) {
|
|
|
21
21
|
ids.add(surfaceId);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
type DisposeHook = (surfaceId: number) => void;
|
|
25
|
+
const disposeHooks: DisposeHook[] = [];
|
|
26
|
+
|
|
27
|
+
export function onSurfaceDispose(cb: DisposeHook) {
|
|
28
|
+
disposeHooks.push(cb);
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
export function untrackSurface(surfaceId: number) {
|
|
25
32
|
const record = surfaces.get(surfaceId);
|
|
26
33
|
if (!record) return;
|
|
@@ -30,6 +37,7 @@ export function untrackSurface(surfaceId: number) {
|
|
|
30
37
|
ids.delete(surfaceId);
|
|
31
38
|
if (ids.size === 0) hostSurfaceIds.delete(record.hostViewId);
|
|
32
39
|
}
|
|
40
|
+
for (const cb of disposeHooks) cb(surfaceId);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export function getOwnedSurface(surfaceId: number, ctx: { viewId: number }): SurfaceRecord | null {
|
|
@@ -54,7 +62,7 @@ export function removeSurfacesForHostView(hostViewId: number) {
|
|
|
54
62
|
for (const surfaceId of Array.from(ids)) {
|
|
55
63
|
const record = surfaces.get(surfaceId);
|
|
56
64
|
if (!record) continue;
|
|
57
|
-
untrackSurface(surfaceId);
|
|
65
|
+
untrackSurface(surfaceId); // fires disposeHooks
|
|
58
66
|
record.view.remove();
|
|
59
67
|
}
|
|
60
68
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Input dispatch helpers — modifier encoding + DOM `key` → Win32 VK + macOS
|
|
2
|
+
// Quartz key code + DOM `code` + char. Backends translate the FFI bitmask
|
|
3
|
+
// (Alt=1, Ctrl=2, Meta=4, Shift=8) to native form. Stage B keymap covers
|
|
4
|
+
// ASCII + the named keys Playwright-style automation relies on.
|
|
5
|
+
|
|
6
|
+
import type { Modifier } from "../../rpc/framework";
|
|
7
|
+
|
|
8
|
+
export function encodeModifiers(mods: Modifier[] | undefined): number {
|
|
9
|
+
if (!mods) return 0;
|
|
10
|
+
let bits = 0;
|
|
11
|
+
for (const m of mods) {
|
|
12
|
+
if (m === "alt") bits |= 1;
|
|
13
|
+
else if (m === "ctrl") bits |= 2;
|
|
14
|
+
else if (m === "meta") bits |= 4;
|
|
15
|
+
else if (m === "shift") bits |= 8;
|
|
16
|
+
}
|
|
17
|
+
return bits;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResolvedKey {
|
|
21
|
+
windowsVkCode: number;
|
|
22
|
+
macKeyCode: number;
|
|
23
|
+
/** DOM `KeyboardEvent.key` — pass-through; native dispatchers forward to engines. */
|
|
24
|
+
key: string;
|
|
25
|
+
/** DOM `KeyboardEvent.code` — derived from US keyboard mapping. */
|
|
26
|
+
code: string;
|
|
27
|
+
/** Text payload for the CHAR / insertText event; empty = skip char. */
|
|
28
|
+
character: string;
|
|
29
|
+
/** Win scancode 0xE0 prefix: nav cluster (Arrow/Insert/Delete/Home/End/
|
|
30
|
+
* PageUp/PageDown/Meta/ContextMenu) AND Numpad-Enter. Distinct from
|
|
31
|
+
* `location` — most extended keys are NOT numpad (location 0). */
|
|
32
|
+
extended: boolean;
|
|
33
|
+
/** DOM `KeyboardEvent.location`: 0=standard, 1=left mod, 2=right mod,
|
|
34
|
+
* 3=numpad. WV2 CDP uses this; CEF derives from scancode 0xE0 prefix. */
|
|
35
|
+
location: 0 | 1 | 2 | 3;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Maps a DOM `KeyboardEvent.key` value to backend-neutral identifiers. */
|
|
39
|
+
export function resolveKey(domKey: string): ResolvedKey {
|
|
40
|
+
if (domKey.length === 0) {
|
|
41
|
+
return { windowsVkCode: 0, macKeyCode: 0, key: "", code: "", character: "", extended: false, location: 0 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Named key (Enter, Tab, ArrowLeft …).
|
|
45
|
+
const named = NAMED_KEYS[domKey];
|
|
46
|
+
if (named) {
|
|
47
|
+
return {
|
|
48
|
+
windowsVkCode: named.win,
|
|
49
|
+
macKeyCode: named.mac,
|
|
50
|
+
key: domKey,
|
|
51
|
+
code: named.code,
|
|
52
|
+
// Space/Tab/Enter generate text in CDP automatically; we pass an explicit
|
|
53
|
+
// character so DOM `keypress` fires consistently across engines.
|
|
54
|
+
character: named.character ?? "",
|
|
55
|
+
extended: named.ext === true,
|
|
56
|
+
location: named.loc ?? 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Single Unicode codepoint — letter / digit / printable / extended.
|
|
61
|
+
if ([...domKey].length === 1) {
|
|
62
|
+
const cp = domKey.codePointAt(0)!;
|
|
63
|
+
// ASCII A-Z / a-z → matching Win VK + mac keyCode + DOM code "KeyX".
|
|
64
|
+
if ((cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A)) {
|
|
65
|
+
const upper = cp & ~0x20; // strip lowercase bit
|
|
66
|
+
return {
|
|
67
|
+
windowsVkCode: upper,
|
|
68
|
+
macKeyCode: MAC_KEY_LETTER[upper - 0x41],
|
|
69
|
+
key: domKey,
|
|
70
|
+
code: `Key${String.fromCharCode(upper)}`,
|
|
71
|
+
character: domKey,
|
|
72
|
+
extended: false,
|
|
73
|
+
location: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ASCII 0-9.
|
|
77
|
+
if (cp >= 0x30 && cp <= 0x39) {
|
|
78
|
+
return {
|
|
79
|
+
windowsVkCode: cp,
|
|
80
|
+
macKeyCode: MAC_KEY_DIGIT[cp - 0x30],
|
|
81
|
+
key: domKey,
|
|
82
|
+
code: `Digit${domKey}`,
|
|
83
|
+
character: domKey,
|
|
84
|
+
extended: false,
|
|
85
|
+
location: 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Other printable codepoint — char event only, no virtual key.
|
|
89
|
+
return { windowsVkCode: 0, macKeyCode: 0, key: domKey, code: "", character: domKey, extended: false, location: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Multi-codepoint string we don't recognise as a named key — pass-through.
|
|
93
|
+
return { windowsVkCode: 0, macKeyCode: 0, key: domKey, code: "", character: "", extended: false, location: 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Win32 VK_* + Quartz Event Services kVK_* + DOM code + literal character +
|
|
97
|
+
// LPARAM extended-key flag. `ext: true` for nav-cluster keys (separate from
|
|
98
|
+
// numpad equivalents) and Numpad-Enter — Chromium derives `KeyboardEvent.code`
|
|
99
|
+
// from LPARAM scancode + extended bit. Sources:
|
|
100
|
+
// learn.microsoft.com/windows/win32/inputdev/virtual-key-codes
|
|
101
|
+
// chromium/ui/events/keycodes/dom/keycode_converter_data.inc
|
|
102
|
+
type NamedKey = { win: number; mac: number; code: string; character?: string; ext?: true; loc?: 1 | 2 | 3 };
|
|
103
|
+
const NAMED_KEYS: Record<string, NamedKey> = {
|
|
104
|
+
Backspace: { win: 0x08, mac: 0x33, code: "Backspace" },
|
|
105
|
+
Tab: { win: 0x09, mac: 0x30, code: "Tab", character: "\t" },
|
|
106
|
+
Enter: { win: 0x0D, mac: 0x24, code: "Enter", character: "\r" },
|
|
107
|
+
NumpadEnter: { win: 0x0D, mac: 0x4C, code: "NumpadEnter", character: "\r", ext: true, loc: 3 },
|
|
108
|
+
Escape: { win: 0x1B, mac: 0x35, code: "Escape" },
|
|
109
|
+
" ": { win: 0x20, mac: 0x31, code: "Space", character: " " },
|
|
110
|
+
Space: { win: 0x20, mac: 0x31, code: "Space", character: " " },
|
|
111
|
+
PageUp: { win: 0x21, mac: 0x74, code: "PageUp", ext: true },
|
|
112
|
+
PageDown: { win: 0x22, mac: 0x79, code: "PageDown", ext: true },
|
|
113
|
+
End: { win: 0x23, mac: 0x77, code: "End", ext: true },
|
|
114
|
+
Home: { win: 0x24, mac: 0x73, code: "Home", ext: true },
|
|
115
|
+
ArrowLeft: { win: 0x25, mac: 0x7B, code: "ArrowLeft", ext: true },
|
|
116
|
+
ArrowUp: { win: 0x26, mac: 0x7E, code: "ArrowUp", ext: true },
|
|
117
|
+
ArrowRight: { win: 0x27, mac: 0x7C, code: "ArrowRight", ext: true },
|
|
118
|
+
ArrowDown: { win: 0x28, mac: 0x7D, code: "ArrowDown", ext: true },
|
|
119
|
+
Insert: { win: 0x2D, mac: 0x72, code: "Insert", ext: true },
|
|
120
|
+
Delete: { win: 0x2E, mac: 0x75, code: "Delete", ext: true },
|
|
121
|
+
Meta: { win: 0x5B, mac: 0x37, code: "MetaLeft", ext: true },
|
|
122
|
+
ContextMenu: { win: 0x5D, mac: 0x6E, code: "ContextMenu", ext: true },
|
|
123
|
+
F1: { win: 0x70, mac: 0x7A, code: "F1" },
|
|
124
|
+
F2: { win: 0x71, mac: 0x78, code: "F2" },
|
|
125
|
+
F3: { win: 0x72, mac: 0x63, code: "F3" },
|
|
126
|
+
F4: { win: 0x73, mac: 0x76, code: "F4" },
|
|
127
|
+
F5: { win: 0x74, mac: 0x60, code: "F5" },
|
|
128
|
+
F6: { win: 0x75, mac: 0x61, code: "F6" },
|
|
129
|
+
F7: { win: 0x76, mac: 0x62, code: "F7" },
|
|
130
|
+
F8: { win: 0x77, mac: 0x64, code: "F8" },
|
|
131
|
+
F9: { win: 0x78, mac: 0x65, code: "F9" },
|
|
132
|
+
F10: { win: 0x79, mac: 0x6D, code: "F10" },
|
|
133
|
+
F11: { win: 0x7A, mac: 0x67, code: "F11" },
|
|
134
|
+
F12: { win: 0x7B, mac: 0x6F, code: "F12" },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// US keyboard layout — Quartz hardware key code per ASCII letter (A → 0x00, …, Z → 0x06).
|
|
138
|
+
const MAC_KEY_LETTER = [
|
|
139
|
+
// A B C D E F G H I J K L M
|
|
140
|
+
0x00, 0x0B, 0x08, 0x02, 0x0E, 0x03, 0x05, 0x04, 0x22, 0x26, 0x28, 0x25, 0x2E,
|
|
141
|
+
// N O P Q R S T U V W X Y Z
|
|
142
|
+
0x2D, 0x1F, 0x23, 0x0C, 0x0F, 0x01, 0x11, 0x20, 0x09, 0x0D, 0x07, 0x10, 0x06,
|
|
143
|
+
];
|
|
144
|
+
// US keyboard layout — Quartz hardware key code per digit (0 → 0x1D, 1 → 0x12 …).
|
|
145
|
+
const MAC_KEY_DIGIT = [
|
|
146
|
+
0x1D, 0x12, 0x13, 0x14, 0x15, 0x17, 0x16, 0x1A, 0x1C, 0x19,
|
|
147
|
+
];
|
|
@@ -8,5 +8,12 @@ export default {
|
|
|
8
8
|
new BuniteEvent("new-window-open", data),
|
|
9
9
|
permissionRequested: (data: { requestId: number; kind: number; url?: string }) =>
|
|
10
10
|
new BuniteEvent("permission-requested", data),
|
|
11
|
-
titleChanged: (data: { detail: string }) => new BuniteEvent("title-changed", data)
|
|
11
|
+
titleChanged: (data: { detail: string }) => new BuniteEvent("title-changed", data),
|
|
12
|
+
loadStart: (data: { detail: string }) => new BuniteEvent("load-start", data),
|
|
13
|
+
loadFinish: (data: { detail: string }) => new BuniteEvent("load-finish", data),
|
|
14
|
+
loadFail: (data: { url: string; reason?: string }) => new BuniteEvent("load-fail", data),
|
|
15
|
+
dialog: (data: { requestId: number; kind: "alert" | "confirm" | "prompt" | "beforeunload"; message: string; defaultPrompt?: string }) =>
|
|
16
|
+
new BuniteEvent("dialog", data),
|
|
17
|
+
consoleMessage: (data: { level: "log" | "warn" | "error" | "info" | "debug"; args: string[]; ts: number }) =>
|
|
18
|
+
new BuniteEvent("console-message", data)
|
|
12
19
|
};
|