getobsrv 0.3.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/bin/obsrv-mcp.js +19 -0
  4. package/bin/obsrv.js +73 -0
  5. package/out/cli/args.js +204 -0
  6. package/out/main/cli.js +600 -0
  7. package/out/main/index.js +539 -0
  8. package/out/main/targetSource-w_vWw7zd.js +351 -0
  9. package/out/mcp/lib.js +175 -0
  10. package/out/mcp/server.js +301 -0
  11. package/out/preload/app.js +87 -0
  12. package/out/preload/sync.js +41 -0
  13. package/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  14. package/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  15. package/out/renderer/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  16. package/out/renderer/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  17. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  18. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  19. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  20. package/out/renderer/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  21. package/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  22. package/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  23. package/out/renderer/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  24. package/out/renderer/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  25. package/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  26. package/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  27. package/out/renderer/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  28. package/out/renderer/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  29. package/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  30. package/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  31. package/out/renderer/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  32. package/out/renderer/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  33. package/out/renderer/assets/index-BAEO9_6W.js +14460 -0
  34. package/out/renderer/assets/index-VleEBVgL.css +446 -0
  35. package/out/renderer/index.html +13 -0
  36. package/out/shared/calibration.js +41 -0
  37. package/out/shared/presets.js +47 -0
  38. package/out/shared/types.js +2 -0
  39. package/package.json +62 -0
  40. package/skills/obsrv-screens/SKILL.md +80 -0
@@ -0,0 +1,539 @@
1
+ "use strict";
2
+ const electron = require("electron");
3
+ const promises = require("node:fs/promises");
4
+ const node_path = require("node:path");
5
+ const targetSource = require("./targetSource-w_vWw7zd.js");
6
+ const node_fs = require("node:fs");
7
+ const node_url = require("node:url");
8
+ const IPC = {
9
+ navigate: "obsrv:navigate",
10
+ reload: "obsrv:reload",
11
+ back: "obsrv:back",
12
+ forward: "obsrv:forward",
13
+ setViewport: "obsrv:set-viewport",
14
+ setNativeBounds: "obsrv:set-native-bounds",
15
+ setMode: "obsrv:set-mode",
16
+ sendInput: "obsrv:send-input",
17
+ getHostInfo: "obsrv:get-host-info",
18
+ getSettings: "obsrv:get-settings",
19
+ setSettings: "obsrv:set-settings",
20
+ frame: "obsrv:frame",
21
+ frameSubscribe: "obsrv:frame-subscribe",
22
+ urlChanged: "obsrv:url-changed",
23
+ loadError: "obsrv:load-error",
24
+ hostChanged: "obsrv:host-changed",
25
+ targetLoading: "obsrv:target-loading",
26
+ targetNavigating: "obsrv:target-navigating",
27
+ syncScroll: "obsrv:sync-scroll",
28
+ applyScroll: "obsrv:apply-scroll",
29
+ openImage: "obsrv:open-image",
30
+ focusUrl: "obsrv:focus-url",
31
+ openImagePath: "obsrv:open-image-path",
32
+ readImageFile: "obsrv:read-image-file"
33
+ };
34
+ function attachFrameBus(target, win) {
35
+ let ready = false;
36
+ let enabled = true;
37
+ const gone = () => win.isDestroyed() || win.webContents.isDestroyed();
38
+ const onFrame = (msg) => {
39
+ if (!ready || !enabled || gone()) return;
40
+ win.webContents.send(IPC.frame, msg);
41
+ };
42
+ const onSubscribe = (e) => {
43
+ if (gone() || e.sender !== win.webContents) return;
44
+ ready = true;
45
+ if (enabled) target.invalidate();
46
+ };
47
+ const onRendererGone = (details) => {
48
+ if (details.isMainFrame && !details.isSameDocument) ready = false;
49
+ };
50
+ target.on("frame", onFrame);
51
+ electron.ipcMain.on(IPC.frameSubscribe, onSubscribe);
52
+ win.webContents.on("did-start-navigation", onRendererGone);
53
+ return {
54
+ detach() {
55
+ ready = false;
56
+ target.off("frame", onFrame);
57
+ electron.ipcMain.removeListener(IPC.frameSubscribe, onSubscribe);
58
+ if (!gone()) win.webContents.off("did-start-navigation", onRendererGone);
59
+ },
60
+ setEnabled(next) {
61
+ enabled = next;
62
+ if (enabled && ready) target.invalidate();
63
+ }
64
+ };
65
+ }
66
+ const MAX_RECT = 16384;
67
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
68
+ const isRecord = (v) => typeof v === "object" && v !== null;
69
+ function parseRect(raw) {
70
+ if (!isRecord(raw)) return null;
71
+ const { x, y, width, height } = raw;
72
+ if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(width) || !isFiniteNumber(height)) return null;
73
+ const r = { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };
74
+ for (const v of [r.x, r.y, r.width, r.height]) if (v < 0 || v > MAX_RECT) return null;
75
+ return r;
76
+ }
77
+ const MODIFIERS = /* @__PURE__ */ new Set([
78
+ "shift",
79
+ "control",
80
+ "alt",
81
+ "meta",
82
+ "leftButtonDown",
83
+ "middleButtonDown",
84
+ "rightButtonDown"
85
+ ]);
86
+ const BUTTONS = /* @__PURE__ */ new Set(["left", "middle", "right"]);
87
+ function parseModifiers(raw) {
88
+ if (!Array.isArray(raw)) return [];
89
+ return raw.filter((m) => typeof m === "string" && MODIFIERS.has(m));
90
+ }
91
+ function parseInputEvent(raw) {
92
+ if (!isRecord(raw)) return null;
93
+ const modifiers = parseModifiers(raw.modifiers);
94
+ switch (raw.type) {
95
+ case "mouseDown":
96
+ case "mouseUp":
97
+ case "mouseMove": {
98
+ const { x, y, button, clickCount } = raw;
99
+ if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(clickCount)) return null;
100
+ if (typeof button !== "string" || !BUTTONS.has(button)) return null;
101
+ return { type: raw.type, x, y, button, clickCount, modifiers };
102
+ }
103
+ case "mouseWheel": {
104
+ const { x, y, deltaX, deltaY } = raw;
105
+ if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(deltaX) || !isFiniteNumber(deltaY)) return null;
106
+ return { type: "mouseWheel", x, y, deltaX, deltaY, modifiers };
107
+ }
108
+ case "keyDown":
109
+ case "keyUp":
110
+ case "char": {
111
+ const { keyCode } = raw;
112
+ if (typeof keyCode !== "string") return null;
113
+ return { type: raw.type, keyCode, modifiers };
114
+ }
115
+ default:
116
+ return null;
117
+ }
118
+ }
119
+ function parseDeviceScaleFactor(raw) {
120
+ if (raw === void 0) return 1;
121
+ if (!isFiniteNumber(raw) || raw < 1 || raw > 4) return null;
122
+ return raw;
123
+ }
124
+ function parseSettings(raw) {
125
+ if (!isRecord(raw)) return null;
126
+ const { hostDiagonalInches, hostNits } = raw;
127
+ if (!isFiniteNumber(hostDiagonalInches) || hostDiagonalInches <= 0) return null;
128
+ if (!isFiniteNumber(hostNits) || hostNits <= 0) return null;
129
+ return { hostDiagonalInches, hostNits };
130
+ }
131
+ function parseMode(raw) {
132
+ return raw === "url" || raw === "image" ? raw : null;
133
+ }
134
+ function parseScrollPos(raw) {
135
+ if (!isRecord(raw)) return null;
136
+ const { x, y } = raw;
137
+ if (!isFiniteNumber(x) || !isFiniteNumber(y) || x < 0 || y < 0) return null;
138
+ return { x, y };
139
+ }
140
+ const isPositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
141
+ function loadSettings(file) {
142
+ try {
143
+ const raw = JSON.parse(node_fs.readFileSync(file, "utf8"));
144
+ return {
145
+ hostDiagonalInches: isPositive(raw.hostDiagonalInches) ? raw.hostDiagonalInches : targetSource.DEFAULT_SETTINGS.hostDiagonalInches,
146
+ hostNits: isPositive(raw.hostNits) ? raw.hostNits : targetSource.DEFAULT_SETTINGS.hostNits
147
+ };
148
+ } catch {
149
+ return { ...targetSource.DEFAULT_SETTINGS };
150
+ }
151
+ }
152
+ function saveSettings(file, s) {
153
+ if (!isPositive(s.hostDiagonalInches) || !isPositive(s.hostNits)) throw new RangeError("settings values must be finite and > 0");
154
+ node_fs.mkdirSync(node_path.dirname(file), { recursive: true });
155
+ node_fs.writeFileSync(file, JSON.stringify(s, null, 2));
156
+ }
157
+ const TOOLBAR_H = 44;
158
+ const MAX_IMAGE_FILE_BYTES = 64 * 1024 * 1024;
159
+ function hostInfo(win) {
160
+ try {
161
+ const d = electron.screen.getDisplayMatching(win.getBounds());
162
+ return {
163
+ physicalWidth: Math.round(d.size.width * d.scaleFactor),
164
+ physicalHeight: Math.round(d.size.height * d.scaleFactor),
165
+ scaleFactor: d.scaleFactor
166
+ };
167
+ } catch {
168
+ return { physicalWidth: 0, physicalHeight: 0, scaleFactor: 0 };
169
+ }
170
+ }
171
+ function registerIpc(ctx) {
172
+ const { win, native, target, bus, sync } = ctx;
173
+ const settingsFile = node_path.join(electron.app.getPath("userData"), "settings.json");
174
+ let settings = loadSettings(settingsFile);
175
+ const fromRenderer = (e) => e.sender === win.webContents;
176
+ const assertRenderer = (e) => {
177
+ if (!fromRenderer(e)) throw new Error("ipc: unexpected sender");
178
+ };
179
+ electron.ipcMain.handle(IPC.navigate, async (e, url) => {
180
+ assertRenderer(e);
181
+ let wanted = url;
182
+ try {
183
+ wanted = targetSource.normalizeUrl(url);
184
+ sync.expect(wanted);
185
+ } catch {
186
+ }
187
+ const [applied] = await Promise.all([native.load(wanted), target.load(wanted)]);
188
+ return applied;
189
+ });
190
+ electron.ipcMain.on(IPC.reload, (e) => {
191
+ if (!fromRenderer(e)) return;
192
+ native.reload();
193
+ target.reload();
194
+ });
195
+ electron.ipcMain.on(IPC.back, (e) => {
196
+ if (!fromRenderer(e)) return;
197
+ native.back();
198
+ });
199
+ electron.ipcMain.on(IPC.forward, (e) => {
200
+ if (!fromRenderer(e)) return;
201
+ native.forward();
202
+ });
203
+ electron.ipcMain.handle(IPC.setViewport, (e, width, height, rawDsf) => {
204
+ assertRenderer(e);
205
+ const dsf = parseDeviceScaleFactor(rawDsf);
206
+ if (dsf === null) throw new Error("invalid deviceScaleFactor");
207
+ const v = target.setViewport(width, height, dsf);
208
+ return { width: v.width, height: v.height };
209
+ });
210
+ electron.ipcMain.on(IPC.sendInput, (e, raw) => {
211
+ if (!fromRenderer(e)) return;
212
+ const ev = parseInputEvent(raw);
213
+ if (!ev) return;
214
+ try {
215
+ target.sendInput(ev);
216
+ } catch {
217
+ }
218
+ });
219
+ electron.ipcMain.on(IPC.setMode, (e, raw) => {
220
+ if (!fromRenderer(e)) return;
221
+ const mode = parseMode(raw);
222
+ if (!mode) return;
223
+ const live = mode === "url";
224
+ native.setVisible(live);
225
+ bus.setEnabled(live);
226
+ });
227
+ let rendererDrivesLayout = false;
228
+ const fallbackLayout = () => {
229
+ if (rendererDrivesLayout) return;
230
+ const [w = 0, h = 0] = win.getContentSize();
231
+ native.setBounds({
232
+ x: 0,
233
+ y: TOOLBAR_H,
234
+ width: Math.floor(w / 2),
235
+ height: Math.max(0, h - TOOLBAR_H)
236
+ });
237
+ };
238
+ fallbackLayout();
239
+ win.on("resize", fallbackLayout);
240
+ electron.ipcMain.on(IPC.setNativeBounds, (e, raw) => {
241
+ if (!fromRenderer(e)) return;
242
+ const rect = parseRect(raw);
243
+ if (!rect) return;
244
+ native.setBounds(rect);
245
+ rendererDrivesLayout = true;
246
+ });
247
+ electron.ipcMain.handle(IPC.getHostInfo, (e) => {
248
+ assertRenderer(e);
249
+ return hostInfo(win);
250
+ });
251
+ let lastHost = JSON.stringify(hostInfo(win));
252
+ const pushHostIfChanged = () => {
253
+ const next = hostInfo(win);
254
+ const key = JSON.stringify(next);
255
+ if (key === lastHost) return;
256
+ lastHost = key;
257
+ if (!win.isDestroyed() && !win.webContents.isDestroyed()) {
258
+ win.webContents.send(IPC.hostChanged, next);
259
+ }
260
+ };
261
+ win.on("move", pushHostIfChanged);
262
+ electron.screen.on("display-metrics-changed", pushHostIfChanged);
263
+ electron.screen.on("display-added", pushHostIfChanged);
264
+ electron.screen.on("display-removed", pushHostIfChanged);
265
+ electron.ipcMain.handle(IPC.getSettings, (e) => {
266
+ assertRenderer(e);
267
+ return settings;
268
+ });
269
+ electron.ipcMain.handle(IPC.setSettings, (e, raw) => {
270
+ assertRenderer(e);
271
+ const s = parseSettings(raw);
272
+ if (!s) throw new Error("invalid settings");
273
+ saveSettings(settingsFile, s);
274
+ settings = s;
275
+ });
276
+ electron.ipcMain.handle(IPC.readImageFile, async (e, raw) => {
277
+ assertRenderer(e);
278
+ if (typeof raw !== "string" || !targetSource.IMAGE_EXTENSIONS.test(raw)) throw new Error("Unsupported file type");
279
+ const { size } = await promises.stat(raw);
280
+ if (size > MAX_IMAGE_FILE_BYTES) {
281
+ throw new Error(`Image file too large (max ${MAX_IMAGE_FILE_BYTES / 1048576} MB)`);
282
+ }
283
+ return promises.readFile(raw);
284
+ });
285
+ }
286
+ function installMenu({ win, native, target }) {
287
+ const send = (channel) => {
288
+ if (!win.isDestroyed()) win.webContents.send(channel);
289
+ };
290
+ const template = [
291
+ { role: "appMenu" },
292
+ {
293
+ label: "File",
294
+ submenu: [
295
+ {
296
+ label: "Open Image…",
297
+ accelerator: "CmdOrCtrl+O",
298
+ click: () => send(IPC.openImage)
299
+ },
300
+ { type: "separator" },
301
+ { role: "close" }
302
+ ]
303
+ },
304
+ { role: "editMenu" },
305
+ {
306
+ label: "View",
307
+ submenu: [
308
+ {
309
+ label: "Reload",
310
+ accelerator: "CmdOrCtrl+R",
311
+ click: () => {
312
+ native.reload();
313
+ target.reload();
314
+ }
315
+ },
316
+ {
317
+ label: "Open Location",
318
+ accelerator: "CmdOrCtrl+L",
319
+ click: () => send(IPC.focusUrl)
320
+ },
321
+ { type: "separator" },
322
+ {
323
+ label: "Toggle Developer Tools",
324
+ accelerator: "Alt+CmdOrCtrl+I",
325
+ click: () => {
326
+ if (!win.isDestroyed()) win.webContents.toggleDevTools();
327
+ }
328
+ },
329
+ { type: "separator" },
330
+ { role: "togglefullscreen" }
331
+ ]
332
+ },
333
+ { role: "windowMenu" }
334
+ ];
335
+ electron.Menu.setApplicationMenu(electron.Menu.buildFromTemplate(template));
336
+ }
337
+ const ERR_ABORTED = -3;
338
+ class NativePane {
339
+ constructor(win, events) {
340
+ this.events = events;
341
+ this.view = new electron.WebContentsView({
342
+ webPreferences: {
343
+ preload: node_path.join(__dirname, "../preload/sync.js"),
344
+ contextIsolation: true,
345
+ sandbox: true,
346
+ nodeIntegration: false
347
+ }
348
+ });
349
+ this.view.setBackgroundColor("#ffffff");
350
+ win.contentView.addChildView(this.view);
351
+ this.view.setBounds(this.bounds);
352
+ const wc = this.view.webContents;
353
+ wc.on("did-fail-load", (_e, code, description, url, isMainFrame) => {
354
+ if (isMainFrame && code !== ERR_ABORTED) this.events.onLoadError({ code, description, url });
355
+ });
356
+ wc.on("will-navigate", (e, url) => {
357
+ const verdict = targetSource.classifyFileNavigation(wc.getURL(), url);
358
+ if (verdict === "allow") return;
359
+ e.preventDefault();
360
+ if (verdict === "image") this.events.onImageDrop(node_url.fileURLToPath(url));
361
+ });
362
+ wc.setWindowOpenHandler(({ url }) => {
363
+ if (url && url !== "about:blank") void this.load(url);
364
+ return { action: "deny" };
365
+ });
366
+ }
367
+ view;
368
+ bounds = { x: 0, y: 0, width: 0, height: 0 };
369
+ visible = true;
370
+ /** Loads URL-bar input; returns the normalised URL that was requested. */
371
+ async load(input) {
372
+ try {
373
+ const url = targetSource.normalizeUrl(input);
374
+ try {
375
+ await this.view.webContents.loadURL(url);
376
+ } catch {
377
+ }
378
+ return url;
379
+ } catch (e) {
380
+ const description = e instanceof Error ? e.message : String(e);
381
+ this.events.onLoadError({ code: 0, description, url: input });
382
+ return input;
383
+ }
384
+ }
385
+ setBounds(rect) {
386
+ this.bounds = rect;
387
+ this.view.setBounds(rect);
388
+ }
389
+ getBounds() {
390
+ return this.bounds;
391
+ }
392
+ /** Image mode draws the left pane in the renderer, so the OS-level view hides. */
393
+ setVisible(visible) {
394
+ this.visible = visible;
395
+ this.view.setVisible(visible);
396
+ }
397
+ isVisible() {
398
+ return this.visible;
399
+ }
400
+ reload() {
401
+ this.view.webContents.reload();
402
+ }
403
+ back() {
404
+ this.view.webContents.navigationHistory.goBack();
405
+ }
406
+ forward() {
407
+ this.view.webContents.navigationHistory.goForward();
408
+ }
409
+ get webContents() {
410
+ return this.view.webContents;
411
+ }
412
+ }
413
+ const LOOP_WINDOW_MS = 1e3;
414
+ const LOOP_ALTERNATIONS = 2;
415
+ function attachSyncBus(native, target, onUrlChanged) {
416
+ const pending = { native: null, target: null };
417
+ let lastReported = "";
418
+ let lastMirror = null;
419
+ let alternations = 0;
420
+ let loopWarned = false;
421
+ function mirror(from, url) {
422
+ const expected = pending[from];
423
+ pending[from] = null;
424
+ if (url !== lastReported || expected !== url) {
425
+ lastReported = url;
426
+ onUrlChanged(url);
427
+ }
428
+ if (expected === url) return;
429
+ const to = from === "native" ? "target" : "native";
430
+ const other = from === "native" ? target : native;
431
+ if (other.webContents.isDestroyed() || other.webContents.getURL() === url) return;
432
+ const now = Date.now();
433
+ if (!lastMirror || now - lastMirror.at >= LOOP_WINDOW_MS) alternations = 0;
434
+ else if (lastMirror.from === to) alternations++;
435
+ lastMirror = { from, at: now };
436
+ if (alternations >= LOOP_ALTERNATIONS) {
437
+ if (!loopWarned) {
438
+ loopWarned = true;
439
+ console.warn(`obsrv: navigation mirror loop broken (${from} -> ${to}: ${url})`);
440
+ }
441
+ return;
442
+ }
443
+ pending[to] = url;
444
+ void other.load(url);
445
+ }
446
+ const onNativeNav = (_e, url) => mirror("native", url);
447
+ const onNativeNavInPage = (_e, url, isMainFrame) => {
448
+ if (isMainFrame) mirror("native", url);
449
+ };
450
+ const onTargetNav = (url) => mirror("target", url);
451
+ native.webContents.on("did-navigate", onNativeNav);
452
+ native.webContents.on("did-navigate-in-page", onNativeNavInPage);
453
+ target.on("url-changed", onTargetNav);
454
+ const onScroll = (e, raw) => {
455
+ const other = e.sender === native.webContents ? target.webContents : e.sender === target.webContents ? native.webContents : null;
456
+ if (!other || other.isDestroyed()) return;
457
+ const pos = parseScrollPos(raw);
458
+ if (!pos) return;
459
+ other.send(IPC.applyScroll, pos);
460
+ };
461
+ electron.ipcMain.on(IPC.syncScroll, onScroll);
462
+ return {
463
+ expect(url) {
464
+ pending.native = url;
465
+ pending.target = url;
466
+ },
467
+ detach() {
468
+ electron.ipcMain.off(IPC.syncScroll, onScroll);
469
+ target.off("url-changed", onTargetNav);
470
+ if (!native.webContents.isDestroyed()) {
471
+ native.webContents.off("did-navigate", onNativeNav);
472
+ native.webContents.off("did-navigate-in-page", onNativeNavInPage);
473
+ }
474
+ }
475
+ };
476
+ }
477
+ function exposeForTests(ctx) {
478
+ if (process.env.OBSRV_TEST === "1") globalThis.__obsrv = ctx;
479
+ }
480
+ function createMainWindow() {
481
+ const win = new electron.BrowserWindow({
482
+ width: 1600,
483
+ height: 1e3,
484
+ minWidth: 900,
485
+ minHeight: 600,
486
+ title: "Obsrv",
487
+ backgroundColor: "#111111",
488
+ show: false,
489
+ webPreferences: {
490
+ preload: node_path.join(__dirname, "../preload/app.js"),
491
+ contextIsolation: true,
492
+ sandbox: true
493
+ }
494
+ });
495
+ win.once("ready-to-show", () => win.show());
496
+ if (!electron.app.isPackaged && process.env.ELECTRON_RENDERER_URL)
497
+ void win.loadURL(process.env.ELECTRON_RENDERER_URL);
498
+ else void win.loadFile(node_path.join(__dirname, "../renderer/index.html"));
499
+ return win;
500
+ }
501
+ function boot() {
502
+ const win = createMainWindow();
503
+ const native = new NativePane(win, {
504
+ onLoadError: (err) => {
505
+ if (!win.isDestroyed()) win.webContents.send(IPC.loadError, err);
506
+ },
507
+ // The renderer reads the file back over `readImageFile`; no bytes here.
508
+ onImageDrop: (path) => {
509
+ if (!win.isDestroyed()) win.webContents.send(IPC.openImagePath, path);
510
+ }
511
+ });
512
+ const target = new targetSource.TargetSource();
513
+ target.on("load-error", (err) => {
514
+ if (!win.isDestroyed()) win.webContents.send(IPC.loadError, err);
515
+ });
516
+ target.on("loading", (loading) => {
517
+ if (!win.isDestroyed()) win.webContents.send(IPC.targetLoading, loading);
518
+ });
519
+ target.on("navigating", () => {
520
+ if (!win.isDestroyed()) win.webContents.send(IPC.targetNavigating);
521
+ });
522
+ const bus = attachFrameBus(target, win);
523
+ const sync = attachSyncBus(native, target, (url) => {
524
+ if (!win.isDestroyed()) win.webContents.send(IPC.urlChanged, url);
525
+ });
526
+ const ctx = { win, native, target, bus, sync };
527
+ registerIpc(ctx);
528
+ installMenu(ctx);
529
+ win.on("close", () => {
530
+ sync.detach();
531
+ bus.detach();
532
+ target.destroy();
533
+ });
534
+ sync.expect("about:blank");
535
+ void native.load("about:blank");
536
+ exposeForTests(ctx);
537
+ }
538
+ void electron.app.whenReady().then(boot);
539
+ electron.app.on("window-all-closed", () => electron.app.quit());