1// @effect-diagnostics nodeBuiltinImport:off -- This desktop-only service resolves its bundled helper beside the Electron entrypoint.
2
3import * as NodePath from "node:path";
4
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import type * as Cause from "effect/Cause";
7import * as Context from "effect/Context";
8import * as Effect from "effect/Effect";
9import * as Exit from "effect/Exit";
10import * as Layer from "effect/Layer";
11import * as Option from "effect/Option";
12import * as Ref from "effect/Ref";
13import * as Schema from "effect/Schema";
14
15import * as Electron from "electron";
16
17import {
18 activateWindowsForeground,
19 isWindowsShellHostedForeground,
20 loadWindowsForegroundApi,
21} from "./WindowsForeground.ts";
22import { startWindowsForegroundFocusThread } from "./WindowsForegroundFocusThread.ts";
23
24function windowsForegroundFocusTarget(window: Electron.BrowserWindow) {
25 return {
26 windowId: window.id,
27 processId: process.pid,
28 title: window.getTitle(),
29 bounds: window.getBounds(),
30 contentBounds: window.getContentBounds(),
31 };
32}
33
34async function isWindowsBrowserWindowForeground(window: Electron.BrowserWindow): Promise<boolean> {
35 const foreground = await loadWindowsForegroundApi()
36 .then((api) => api.getForegroundWindow())
37 .catch(() => undefined);
38 if (window.isDestroyed() || foreground === undefined || foreground === 0n) return false;
39 const handle = window.getNativeWindowHandle();
40 const hwnd = handle.length === 8 ? handle.readBigUInt64LE() : BigInt(handle.readUInt32LE());
41 return foreground === hwnd;
42}
43
44const ElectronWindowCreateOptions = Schema.Struct({
45 title: Schema.NullOr(Schema.String),
46 width: Schema.NullOr(Schema.Number),
47 height: Schema.NullOr(Schema.Number),
48 minWidth: Schema.NullOr(Schema.Number),
49 minHeight: Schema.NullOr(Schema.Number),
50 show: Schema.NullOr(Schema.Boolean),
51 modal: Schema.NullOr(Schema.Boolean),
52 frame: Schema.NullOr(Schema.Boolean),
53 transparent: Schema.NullOr(Schema.Boolean),
54 backgroundColor: Schema.NullOr(Schema.String),
55 webPreferences: Schema.Struct({
56 preload: Schema.NullOr(Schema.String),
57 partition: Schema.NullOr(Schema.String),
58 backgroundThrottling: Schema.NullOr(Schema.Boolean),
59 sandbox: Schema.NullOr(Schema.Boolean),
60 contextIsolation: Schema.NullOr(Schema.Boolean),
61 nodeIntegration: Schema.NullOr(Schema.Boolean),
62 webviewTag: Schema.NullOr(Schema.Boolean),
63 }),
64});
65
66const ElectronWindowOperation = Schema.Literals([
67 "list-windows",
68 "get-focused-window",
69 "inspect-window",
70 "reveal-window",
71 "send-window-message",
72 "destroy-window",
73]);
74
75export class ElectronWindowCreateError extends Schema.TaggedError<ElectronWindowCreateError>()(
76 "ElectronWindowCreateError",
77 {
78 options: ElectronWindowCreateOptions,
79 cause: Schema.Defect(),
80 },
81) {
82 override get message(): string {
83 const title = this.options.title === null ? "" : ` "${this.options.title}"`;
84 const dimensions =
85 this.options.width === null || this.options.height === null
86 ? ""
87 : ` (${this.options.width}x${this.options.height})`;
88 return `Failed to create Electron BrowserWindow${title}${dimensions}.`;
89 }
90}
91
92export class ElectronWindowOperationError extends Schema.TaggedError<ElectronWindowOperationError>()(
93 "ElectronWindowOperationError",
94 {
95 operation: ElectronWindowOperation,
96 platform: Schema.String,
97 windowId: Schema.NullOr(Schema.Number),
98 channel: Schema.NullOr(Schema.String),
99 cause: Schema.Defect(),
100 },
101) {
102 override get message(): string {
103 const window = this.windowId === null ? "" : ` for window ${this.windowId}`;
104 const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`;
105 return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`;
106 }
107}
108
109export class ElectronWindow extends Context.Service<
110 ElectronWindow,
111 {
112 readonly create: (
113 options: Electron.BrowserWindowConstructorOptions,
114 ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
115 readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
116 readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
117 readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
118 readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;
119 readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;
120 readonly prepareReveal: (window: Electron.BrowserWindow) => Effect.Effect<boolean>;
121 readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;
122 readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
123 readonly destroyAll: Effect.Effect<void>;
124 readonly syncAllAppearance: <E, R>(
125 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
126 ) => Effect.Effect<void, E, R>;
127 }
128>()("@t3tools/desktop/electron/ElectronWindow") {}
129
130/** @public Service construction is part of the canonical Effect module API. */
131export const make = Effect.gen(function* () {
132 const platform = yield* HostProcess.Platform;
133 // The focus worker loads a native accessibility module. Start it on the first
134 // capture reveal so users who never capture pay nothing at launch.
135 let windowsForegroundFocus: ReturnType<typeof startWindowsForegroundFocusThread> | undefined;
136 const ensureWindowsForegroundFocus = () => {
137 windowsForegroundFocus ??= startWindowsForegroundFocusThread(
138 NodePath.join(__dirname, "electron", "WindowsForegroundFocusWorker.cjs"),
139 );
140 return windowsForegroundFocus;
141 };
142 // Tracks a capture reveal in flight. Ordinary reveals keep Electron's native path.
143 const captureRevealWindows = new Set<number>();
144 yield* Effect.addFinalizer(() => Effect.sync(() => windowsForegroundFocus?.close()));
145 const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
146
147 const listWindows = Effect.try({
148 try: () => Electron.BrowserWindow.getAllWindows(),
149 catch: (cause) =>
150 new ElectronWindowOperationError({
151 operation: "list-windows",
152 platform,
153 windowId: null,
154 channel: null,
155 cause,
156 }),
157 }).pipe(Effect.orDie);
158
159 const isWindowDestroyed = (window: Electron.BrowserWindow) =>
160 Effect.try({
161 try: () => window.isDestroyed(),
162 catch: (cause) =>
163 new ElectronWindowOperationError({
164 operation: "inspect-window",
165 platform,
166 windowId: window.id,
167 channel: null,
168 cause,
169 }),
170 }).pipe(Effect.orDie);
171
172 const liveMain = Effect.gen(function* () {
173 const main = yield* Ref.get(mainWindowRef);
174 if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) {
175 return Option.none<Electron.BrowserWindow>();
176 }
177 return main;
178 });
179
180 const currentMainOrFirst = Effect.gen(function* () {
181 const main = yield* liveMain;
182 if (Option.isSome(main)) {
183 return main;
184 }
185
186 const first = Option.fromNullishOr((yield* listWindows)[0] ?? null);
187 if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) {
188 return Option.none<Electron.BrowserWindow>();
189 }
190 return first;
191 });
192
193 const focusedMainOrFirst = Effect.gen(function* () {
194 const focused = yield* Effect.try({
195 try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null),
196 catch: (cause) =>
197 new ElectronWindowOperationError({
198 operation: "get-focused-window",
199 platform,
200 windowId: null,
201 channel: null,
202 cause,
203 }),
204 }).pipe(Effect.orDie);
205 if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) {
206 return focused;
207 }
208 return yield* currentMainOrFirst;
209 });
210
211 return ElectronWindow.of({
212 create: (options) => {
213 const webPreferences = options.webPreferences;
214 const diagnosticOptions = {
215 title: options.title ?? null,
216 width: options.width ?? null,
217 height: options.height ?? null,
218 minWidth: options.minWidth ?? null,
219 minHeight: options.minHeight ?? null,
220 show: options.show ?? null,
221 modal: options.modal ?? null,
222 frame: options.frame ?? null,
223 transparent: options.transparent ?? null,
224 backgroundColor: options.backgroundColor ?? null,
225 webPreferences: {
226 preload: webPreferences?.preload ?? null,
227 partition: webPreferences?.partition ?? null,
228 backgroundThrottling: webPreferences?.backgroundThrottling ?? null,
229 sandbox: webPreferences?.sandbox ?? null,
230 contextIsolation: webPreferences?.contextIsolation ?? null,
231 nodeIntegration: webPreferences?.nodeIntegration ?? null,
232 webviewTag: webPreferences?.webviewTag ?? null,
233 },
234 } satisfies typeof ElectronWindowCreateOptions.Type;
235
236 return Effect.try({
237 try: () => new Electron.BrowserWindow(options),
238 catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }),
239 });
240 },
241 main: liveMain,
242 currentMainOrFirst,
243 focusedMainOrFirst,
244 setMain: (window) => Ref.set(mainWindowRef, Option.some(window)),
245 clearMain: (window) =>
246 Ref.update(mainWindowRef, (current) => {
247 if (Option.isNone(current)) {
248 return current;
249 }
250 if (Option.isSome(window) && current.value !== window.value) {
251 return current;
252 }
253 return Option.none();
254 }),
255 prepareReveal: (window) =>
256 Effect.promise(async () => {
257 if (platform !== "win32" || window.isDestroyed()) {
258 return false;
259 }
260 captureRevealWindows.add(window.id);
261 return ensureWindowsForegroundFocus()
262 .prepare(windowsForegroundFocusTarget(window))
263 .catch(() => false);
264 }),
265 reveal: (window) =>
266 Effect.tryPromise({
267 try: async () => {
268 if (window.isDestroyed()) {
269 return;
270 }
271
272 // Only a capture reveal fights another process for the foreground, which
273 // needs Win32 calls that load native modules. Everything else stays native.
274 const captureReveal = platform === "win32" && captureRevealWindows.delete(window.id);
275 const shellHostedForeground =
276 captureReveal && (await isWindowsShellHostedForeground().catch(() => false));
277
278 if (window.isMinimized()) {
279 window.restore();
280 }
281
282 if (captureReveal) {
283 Electron.app.focus();
284 }
285
286 if (captureReveal || !window.isVisible()) {
287 window.show();
288 }
289
290 if (captureReveal) {
291 window.moveTop();
292 }
293
294 if (platform === "darwin") {
295 Electron.app.focus({ steal: true });
296 }
297
298 window.focus();
299
300 if (captureReveal) {
301 if (shellHostedForeground) {
302 await windowsForegroundFocus
303 ?.focus(windowsForegroundFocusTarget(window))
304 .catch(() => false);
305 }
306 try {
307 await activateWindowsForeground(window.getNativeWindowHandle());
308 } catch {
309 const needsFocus = !(await isWindowsBrowserWindowForeground(window));
310 if (needsFocus && !window.isDestroyed()) {
311 await windowsForegroundFocus
312 ?.focus(windowsForegroundFocusTarget(window))
313 .catch(() => false);
314 }
315 if (!window.isDestroyed()) {
316 await activateWindowsForeground(window.getNativeWindowHandle());
317 }
318 }
319 }
320 },
321 catch: (cause) =>
322 new ElectronWindowOperationError({
323 operation: "reveal-window",
324 platform,
325 windowId: window.id,
326 channel: null,
327 cause,
328 }),
329 }).pipe(Effect.orDie),
330 sendAll: (channel, ...args) =>
331 Effect.gen(function* () {
332 for (const window of yield* listWindows) {
333 if (yield* isWindowDestroyed(window)) {
334 continue;
335 }
336 yield* Effect.try({
337 try: () => window.webContents.send(channel, ...args),
338 catch: (cause) =>
339 new ElectronWindowOperationError({
340 operation: "send-window-message",
341 platform,
342 windowId: window.id,
343 channel,
344 cause,
345 }),
346 }).pipe(Effect.orDie);
347 }
348 }),
349 destroyAll: Effect.gen(function* () {
350 let firstFailure: Cause.Cause<never> | undefined;
351 for (const window of yield* listWindows) {
352 const exit = yield* Effect.exit(
353 Effect.try({
354 try: () => window.destroy(),
355 catch: (cause) =>
356 new ElectronWindowOperationError({
357 operation: "destroy-window",
358 platform,
359 windowId: window.id,
360 channel: null,
361 cause,
362 }),
363 }).pipe(Effect.orDie),
364 );
365 if (Exit.isFailure(exit)) {
366 firstFailure ??= exit.cause;
367 }
368 }
369 if (firstFailure !== undefined) {
370 return yield* Effect.failCause(firstFailure);
371 }
372 }),
373 syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>(
374 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
375 ) {
376 const windows = yield* listWindows;
377 for (const window of windows) {
378 if (yield* isWindowDestroyed(window)) {
379 continue;
380 }
381 yield* sync(window);
382 }
383 }),
384 });
385});
386
387export const layer = Layer.effect(ElectronWindow, make);