| author | |
| committer | |
| log | c7b375e9b0f0581a977a2745e3afbf5732aecb15 |
| tree | 4fb0a87e66e0eb7d6ab0bd185b3e0d4a19c936e5 |
| parent | abc253d97dc1d976cf15e69276f2ac7375b26310 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>2 files changed, 385 insertions(+), 54 deletions(-)
apps/desktop/src/electron/ElectronWindow.test.ts+194-10| ... | @@ -1,26 +1,39 @@ | ... | @@ -1,26 +1,39 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 3 | import * as Cause from "effect/Cause"; | ||
| 2 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; | ||
| 3 | import type * as Electron from "electron"; | 6 | import type * as Electron from "electron"; |
| 4 | import { beforeEach, vi } from "vite-plus/test"; | 7 | import { beforeEach, vi } from "vite-plus/test"; |
| 5 | 8 | ||
| 6 | const { appFocusMock, getAllWindowsMock } = vi.hoisted(() => ({ | 9 | const { appFocusMock, browserWindowMock, getAllWindowsMock, getFocusedWindowMock } = vi.hoisted( |
| 7 | appFocusMock: vi.fn(), | 10 | () => ({ |
| 8 | getAllWindowsMock: vi.fn(), | 11 | appFocusMock: vi.fn(), |
| 9 | })); | 12 | browserWindowMock: vi.fn(function BrowserWindowMock() {}), |
| 13 | getAllWindowsMock: vi.fn(), | ||
| 14 | getFocusedWindowMock: vi.fn(), | ||
| 15 | }), | ||
| 16 | ); | ||
| 10 | 17 | ||
| 11 | vi.mock("electron", () => ({ | 18 | vi.mock("electron", () => ({ |
| 12 | app: { | 19 | app: { |
| 13 | focus: appFocusMock, | 20 | focus: appFocusMock, |
| 14 | }, | 21 | }, |
| 15 | BrowserWindow: { | 22 | BrowserWindow: Object.assign(browserWindowMock, { |
| 16 | getAllWindows: getAllWindowsMock, | 23 | getAllWindows: getAllWindowsMock, |
| 17 | }, | 24 | getFocusedWindow: getFocusedWindowMock, |
| 25 | }), | ||
| 18 | })); | 26 | })); |
| 19 | 27 | ||
| 20 | import * as ElectronWindow from "./ElectronWindow.ts"; | 28 | import * as ElectronWindow from "./ElectronWindow.ts"; |
| 21 | 29 | ||
| 22 | function makeBrowserWindow(input: { readonly destroyed: boolean }) { | 30 | const TestLayer = ElectronWindow.layer.pipe( |
| 31 | Layer.provide(Layer.succeed(HostProcessPlatform, "linux")), | ||
| 32 | ); | ||
| 33 | |||
| 34 | function makeBrowserWindow(input: { readonly id: number; readonly destroyed: boolean }) { | ||
| 23 | return { | 35 | return { |
| 36 | id: input.id, | ||
| 24 | isDestroyed: vi.fn(() => input.destroyed), | 37 | isDestroyed: vi.fn(() => input.destroyed), |
| 25 | } as unknown as Electron.BrowserWindow; | 38 | } as unknown as Electron.BrowserWindow; |
| 26 | } | 39 | } |
| ... | @@ -28,13 +41,78 @@ function makeBrowserWindow(input: { readonly destroyed: boolean }) { | ... | @@ -28,13 +41,78 @@ function makeBrowserWindow(input: { readonly destroyed: boolean }) { |
| 28 | describe("ElectronWindow", () => { | 41 | describe("ElectronWindow", () => { |
| 29 | beforeEach(() => { | 42 | beforeEach(() => { |
| 30 | appFocusMock.mockReset(); | 43 | appFocusMock.mockReset(); |
| 44 | browserWindowMock.mockReset(); | ||
| 31 | getAllWindowsMock.mockReset(); | 45 | getAllWindowsMock.mockReset(); |
| 46 | getFocusedWindowMock.mockReset(); | ||
| 32 | }); | 47 | }); |
| 33 | 48 | ||
| 49 | it.effect("preserves schema-safe creation context and the Electron cause", () => | ||
| 50 | Effect.gen(function* () { | ||
| 51 | const cause = new Error("native BrowserWindow construction failed"); | ||
| 52 | browserWindowMock.mockImplementationOnce(function BrowserWindowFailure() { | ||
| 53 | throw cause; | ||
| 54 | }); | ||
| 55 | const options = { | ||
| 56 | title: "T3 Code", | ||
| 57 | width: 1100, | ||
| 58 | height: 780, | ||
| 59 | minWidth: 840, | ||
| 60 | minHeight: 620, | ||
| 61 | show: false, | ||
| 62 | modal: false, | ||
| 63 | frame: true, | ||
| 64 | transparent: false, | ||
| 65 | backgroundColor: "#101010", | ||
| 66 | icon: {} as Electron.NativeImage, | ||
| 67 | webPreferences: { | ||
| 68 | preload: "/tmp/preload.js", | ||
| 69 | partition: "persist:t3code-preview-test", | ||
| 70 | sandbox: true, | ||
| 71 | contextIsolation: true, | ||
| 72 | nodeIntegration: false, | ||
| 73 | webviewTag: true, | ||
| 74 | spellcheck: true, | ||
| 75 | }, | ||
| 76 | } satisfies Electron.BrowserWindowConstructorOptions; | ||
| 77 | const electronWindow = yield* ElectronWindow.ElectronWindow; | ||
| 78 | |||
| 79 | const error = yield* electronWindow.create(options).pipe(Effect.flip); | ||
| 80 | |||
| 81 | assert.instanceOf(error, ElectronWindow.ElectronWindowCreateError); | ||
| 82 | assert.isTrue(ElectronWindow.isElectronWindowCreateError(error)); | ||
| 83 | assert.deepEqual(error.options, { | ||
| 84 | title: "T3 Code", | ||
| 85 | width: 1100, | ||
| 86 | height: 780, | ||
| 87 | minWidth: 840, | ||
| 88 | minHeight: 620, | ||
| 89 | show: false, | ||
| 90 | modal: false, | ||
| 91 | frame: true, | ||
| 92 | transparent: false, | ||
| 93 | backgroundColor: "#101010", | ||
| 94 | webPreferences: { | ||
| 95 | preload: "/tmp/preload.js", | ||
| 96 | partition: "persist:t3code-preview-test", | ||
| 97 | sandbox: true, | ||
| 98 | contextIsolation: true, | ||
| 99 | nodeIntegration: false, | ||
| 100 | webviewTag: true, | ||
| 101 | }, | ||
| 102 | }); | ||
| 103 | assert.isFalse("icon" in error.options); | ||
| 104 | assert.isFalse("spellcheck" in error.options.webPreferences); | ||
| 105 | assert.strictEqual(error.cause, cause); | ||
| 106 | assert.equal(error.message, 'Failed to create Electron BrowserWindow "T3 Code" (1100x780).'); | ||
| 107 | assert.notInclude(error.message, cause.message); | ||
| 108 | assert.deepEqual(browserWindowMock.mock.calls, [[options]]); | ||
| 109 | }).pipe(Effect.provide(TestLayer)), | ||
| 110 | ); | ||
| 111 | |||
| 34 | it.effect("skips windows destroyed before appearance sync runs", () => | 112 | it.effect("skips windows destroyed before appearance sync runs", () => |
| 35 | Effect.gen(function* () { | 113 | Effect.gen(function* () { |
| 36 | const liveWindow = makeBrowserWindow({ destroyed: false }); | 114 | const liveWindow = makeBrowserWindow({ id: 1, destroyed: false }); |
| 37 | const destroyedWindow = makeBrowserWindow({ destroyed: true }); | 115 | const destroyedWindow = makeBrowserWindow({ id: 2, destroyed: true }); |
| 38 | getAllWindowsMock.mockReturnValue([destroyedWindow, liveWindow]); | 116 | getAllWindowsMock.mockReturnValue([destroyedWindow, liveWindow]); |
| 39 | 117 | ||
| 40 | const syncedWindows: Electron.BrowserWindow[] = []; | 118 | const syncedWindows: Electron.BrowserWindow[] = []; |
| ... | @@ -46,6 +124,112 @@ describe("ElectronWindow", () => { | ... | @@ -46,6 +124,112 @@ describe("ElectronWindow", () => { |
| 46 | ); | 124 | ); |
| 47 | 125 | ||
| 48 | assert.deepEqual(syncedWindows, [liveWindow]); | 126 | assert.deepEqual(syncedWindows, [liveWindow]); |
| 49 | }).pipe(Effect.provide(ElectronWindow.layer)), | 127 | }).pipe(Effect.provide(TestLayer)), |
| 128 | ); | ||
| 129 | |||
| 130 | it.effect("preserves window enumeration failures as structured defects", () => | ||
| 131 | Effect.gen(function* () { | ||
| 132 | const cause = new Error("window enumeration failed"); | ||
| 133 | getAllWindowsMock.mockImplementationOnce(() => { | ||
| 134 | throw cause; | ||
| 135 | }); | ||
| 136 | |||
| 137 | const electronWindow = yield* ElectronWindow.ElectronWindow; | ||
| 138 | const exit = yield* Effect.exit(electronWindow.currentMainOrFirst); | ||
| 139 | |||
| 140 | assert.equal(exit._tag, "Failure"); | ||
| 141 | if (exit._tag === "Failure") { | ||
| 142 | const error = Cause.squash(exit.cause); | ||
| 143 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | ||
| 144 | assert.equal(error.operation, "list-windows"); | ||
| 145 | assert.equal(error.platform, "linux"); | ||
| 146 | assert.isNull(error.windowId); | ||
| 147 | assert.isNull(error.channel); | ||
| 148 | assert.strictEqual(error.cause, cause); | ||
| 149 | assert.notInclude(error.message, cause.message); | ||
| 150 | } | ||
| 151 | }).pipe(Effect.provide(TestLayer)), | ||
| 152 | ); | ||
| 153 | |||
| 154 | it.effect("preserves reveal failures with the target window", () => | ||
| 155 | Effect.gen(function* () { | ||
| 156 | const cause = new Error("window restore failed"); | ||
| 157 | const window = { | ||
| 158 | id: 41, | ||
| 159 | isDestroyed: vi.fn(() => false), | ||
| 160 | isMinimized: vi.fn(() => true), | ||
| 161 | restore: vi.fn(() => { | ||
| 162 | throw cause; | ||
| 163 | }), | ||
| 164 | } as unknown as Electron.BrowserWindow; | ||
| 165 | |||
| 166 | const electronWindow = yield* ElectronWindow.ElectronWindow; | ||
| 167 | const exit = yield* Effect.exit(electronWindow.reveal(window)); | ||
| 168 | |||
| 169 | assert.equal(exit._tag, "Failure"); | ||
| 170 | if (exit._tag === "Failure") { | ||
| 171 | const error = Cause.squash(exit.cause); | ||
| 172 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | ||
| 173 | assert.equal(error.operation, "reveal-window"); | ||
| 174 | assert.equal(error.windowId, 41); | ||
| 175 | assert.isNull(error.channel); | ||
| 176 | assert.strictEqual(error.cause, cause); | ||
| 177 | } | ||
| 178 | }).pipe(Effect.provide(TestLayer)), | ||
| 179 | ); | ||
| 180 | |||
| 181 | it.effect("preserves message delivery failures with window and channel context", () => | ||
| 182 | Effect.gen(function* () { | ||
| 183 | const cause = new Error("renderer send failed"); | ||
| 184 | const window = { | ||
| 185 | id: 42, | ||
| 186 | isDestroyed: vi.fn(() => false), | ||
| 187 | webContents: { | ||
| 188 | send: vi.fn(() => { | ||
| 189 | throw cause; | ||
| 190 | }), | ||
| 191 | }, | ||
| 192 | } as unknown as Electron.BrowserWindow; | ||
| 193 | getAllWindowsMock.mockReturnValueOnce([window]); | ||
| 194 | |||
| 195 | const electronWindow = yield* ElectronWindow.ElectronWindow; | ||
| 196 | const exit = yield* Effect.exit(electronWindow.sendAll("desktop:update", { ready: true })); | ||
| 197 | |||
| 198 | assert.equal(exit._tag, "Failure"); | ||
| 199 | if (exit._tag === "Failure") { | ||
| 200 | const error = Cause.squash(exit.cause); | ||
| 201 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | ||
| 202 | assert.equal(error.operation, "send-window-message"); | ||
| 203 | assert.equal(error.windowId, 42); | ||
| 204 | assert.equal(error.channel, "desktop:update"); | ||
| 205 | assert.strictEqual(error.cause, cause); | ||
| 206 | } | ||
| 207 | }).pipe(Effect.provide(TestLayer)), | ||
| 208 | ); | ||
| 209 | |||
| 210 | it.effect("preserves destroy failures with the target window", () => | ||
| 211 | Effect.gen(function* () { | ||
| 212 | const cause = new Error("window destroy failed"); | ||
| 213 | const window = { | ||
| 214 | id: 43, | ||
| 215 | destroy: vi.fn(() => { | ||
| 216 | throw cause; | ||
| 217 | }), | ||
| 218 | } as unknown as Electron.BrowserWindow; | ||
| 219 | getAllWindowsMock.mockReturnValueOnce([window]); | ||
| 220 | |||
| 221 | const electronWindow = yield* ElectronWindow.ElectronWindow; | ||
| 222 | const exit = yield* Effect.exit(electronWindow.destroyAll); | ||
| 223 | |||
| 224 | assert.equal(exit._tag, "Failure"); | ||
| 225 | if (exit._tag === "Failure") { | ||
| 226 | const error = Cause.squash(exit.cause); | ||
| 227 | assert.instanceOf(error, ElectronWindow.ElectronWindowOperationError); | ||
| 228 | assert.equal(error.operation, "destroy-window"); | ||
| 229 | assert.equal(error.windowId, 43); | ||
| 230 | assert.isNull(error.channel); | ||
| 231 | assert.strictEqual(error.cause, cause); | ||
| 232 | } | ||
| 233 | }).pipe(Effect.provide(TestLayer)), | ||
| 50 | ); | 234 | ); |
| 51 | }); | 235 | }); |
apps/desktop/src/electron/ElectronWindow.ts+191-44| ... | @@ -8,14 +8,69 @@ import * as Schema from "effect/Schema"; | ... | @@ -8,14 +8,69 @@ import * as Schema from "effect/Schema"; |
| 8 | 8 | ||
| 9 | import * as Electron from "electron"; | 9 | import * as Electron from "electron"; |
| 10 | 10 | ||
| 11 | const ElectronWindowCreateOptions = Schema.Struct({ | ||
| 12 | title: Schema.NullOr(Schema.String), | ||
| 13 | width: Schema.NullOr(Schema.Number), | ||
| 14 | height: Schema.NullOr(Schema.Number), | ||
| 15 | minWidth: Schema.NullOr(Schema.Number), | ||
| 16 | minHeight: Schema.NullOr(Schema.Number), | ||
| 17 | show: Schema.NullOr(Schema.Boolean), | ||
| 18 | modal: Schema.NullOr(Schema.Boolean), | ||
| 19 | frame: Schema.NullOr(Schema.Boolean), | ||
| 20 | transparent: Schema.NullOr(Schema.Boolean), | ||
| 21 | backgroundColor: Schema.NullOr(Schema.String), | ||
| 22 | webPreferences: Schema.Struct({ | ||
| 23 | preload: Schema.NullOr(Schema.String), | ||
| 24 | partition: Schema.NullOr(Schema.String), | ||
| 25 | sandbox: Schema.NullOr(Schema.Boolean), | ||
| 26 | contextIsolation: Schema.NullOr(Schema.Boolean), | ||
| 27 | nodeIntegration: Schema.NullOr(Schema.Boolean), | ||
| 28 | webviewTag: Schema.NullOr(Schema.Boolean), | ||
| 29 | }), | ||
| 30 | }); | ||
| 31 | |||
| 32 | const ElectronWindowOperation = Schema.Literals([ | ||
| 33 | "list-windows", | ||
| 34 | "get-focused-window", | ||
| 35 | "inspect-window", | ||
| 36 | "reveal-window", | ||
| 37 | "send-window-message", | ||
| 38 | "destroy-window", | ||
| 39 | ]); | ||
| 40 | |||
| 11 | export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()( | 41 | export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()( |
| 12 | "ElectronWindowCreateError", | 42 | "ElectronWindowCreateError", |
| 13 | { | 43 | { |
| 44 | options: ElectronWindowCreateOptions, | ||
| 45 | cause: Schema.Defect(), | ||
| 46 | }, | ||
| 47 | ) { | ||
| 48 | override get message(): string { | ||
| 49 | const title = this.options.title === null ? "" : ` "${this.options.title}"`; | ||
| 50 | const dimensions = | ||
| 51 | this.options.width === null || this.options.height === null | ||
| 52 | ? "" | ||
| 53 | : ` (${this.options.width}x${this.options.height})`; | ||
| 54 | return `Failed to create Electron BrowserWindow${title}${dimensions}.`; | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError); | ||
| 59 | |||
| 60 | export class ElectronWindowOperationError extends Schema.TaggedErrorClass<ElectronWindowOperationError>()( | ||
| 61 | "ElectronWindowOperationError", | ||
| 62 | { | ||
| 63 | operation: ElectronWindowOperation, | ||
| 64 | platform: Schema.String, | ||
| 65 | windowId: Schema.NullOr(Schema.Number), | ||
| 66 | channel: Schema.NullOr(Schema.String), | ||
| 14 | cause: Schema.Defect(), | 67 | cause: Schema.Defect(), |
| 15 | }, | 68 | }, |
| 16 | ) { | 69 | ) { |
| 17 | override get message(): string { | 70 | override get message(): string { |
| 18 | return "Failed to create Electron BrowserWindow."; | 71 | const window = this.windowId === null ? "" : ` for window ${this.windowId}`; |
| 72 | const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`; | ||
| 73 | return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`; | ||
| 19 | } | 74 | } |
| 20 | } | 75 | } |
| 21 | 76 | ||
| ... | @@ -43,9 +98,38 @@ export const make = Effect.gen(function* () { | ... | @@ -43,9 +98,38 @@ export const make = Effect.gen(function* () { |
| 43 | const platform = yield* HostProcessPlatform; | 98 | const platform = yield* HostProcessPlatform; |
| 44 | const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); | 99 | const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); |
| 45 | 100 | ||
| 46 | const liveMain = Ref.get(mainWindowRef).pipe( | 101 | const listWindows = Effect.try({ |
| 47 | Effect.map(Option.filter((value) => !value.isDestroyed())), | 102 | try: () => Electron.BrowserWindow.getAllWindows(), |
| 48 | ); | 103 | catch: (cause) => |
| 104 | new ElectronWindowOperationError({ | ||
| 105 | operation: "list-windows", | ||
| 106 | platform, | ||
| 107 | windowId: null, | ||
| 108 | channel: null, | ||
| 109 | cause, | ||
| 110 | }), | ||
| 111 | }).pipe(Effect.orDie); | ||
| 112 | |||
| 113 | const isWindowDestroyed = (window: Electron.BrowserWindow) => | ||
| 114 | Effect.try({ | ||
| 115 | try: () => window.isDestroyed(), | ||
| 116 | catch: (cause) => | ||
| 117 | new ElectronWindowOperationError({ | ||
| 118 | operation: "inspect-window", | ||
| 119 | platform, | ||
| 120 | windowId: window.id, | ||
| 121 | channel: null, | ||
| 122 | cause, | ||
| 123 | }), | ||
| 124 | }).pipe(Effect.orDie); | ||
| 125 | |||
| 126 | const liveMain = Effect.gen(function* () { | ||
| 127 | const main = yield* Ref.get(mainWindowRef); | ||
| 128 | if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) { | ||
| 129 | return Option.none<Electron.BrowserWindow>(); | ||
| 130 | } | ||
| 131 | return main; | ||
| 132 | }); | ||
| 49 | 133 | ||
| 50 | const currentMainOrFirst = Effect.gen(function* () { | 134 | const currentMainOrFirst = Effect.gen(function* () { |
| 51 | const main = yield* liveMain; | 135 | const main = yield* liveMain; |
| ... | @@ -53,27 +137,60 @@ export const make = Effect.gen(function* () { | ... | @@ -53,27 +137,60 @@ export const make = Effect.gen(function* () { |
| 53 | return main; | 137 | return main; |
| 54 | } | 138 | } |
| 55 | 139 | ||
| 56 | return Option.fromNullishOr(Electron.BrowserWindow.getAllWindows()[0] ?? null).pipe( | 140 | const first = Option.fromNullishOr((yield* listWindows)[0] ?? null); |
| 57 | Option.filter((window) => !window.isDestroyed()), | 141 | if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) { |
| 58 | ); | 142 | return Option.none<Electron.BrowserWindow>(); |
| 143 | } | ||
| 144 | return first; | ||
| 59 | }); | 145 | }); |
| 60 | 146 | ||
| 61 | const focusedMainOrFirst = Effect.sync(() => | 147 | const focusedMainOrFirst = Effect.gen(function* () { |
| 62 | Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null).pipe( | 148 | const focused = yield* Effect.try({ |
| 63 | Option.filter((window) => !window.isDestroyed()), | 149 | try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null), |
| 64 | ), | 150 | catch: (cause) => |
| 65 | ).pipe( | 151 | new ElectronWindowOperationError({ |
| 66 | Effect.flatMap((focused) => | 152 | operation: "get-focused-window", |
| 67 | Option.isSome(focused) ? Effect.succeed(focused) : currentMainOrFirst, | 153 | platform, |
| 68 | ), | 154 | windowId: null, |
| 69 | ); | 155 | channel: null, |
| 156 | cause, | ||
| 157 | }), | ||
| 158 | }).pipe(Effect.orDie); | ||
| 159 | if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) { | ||
| 160 | return focused; | ||
| 161 | } | ||
| 162 | return yield* currentMainOrFirst; | ||
| 163 | }); | ||
| 70 | 164 | ||
| 71 | return ElectronWindow.of({ | 165 | return ElectronWindow.of({ |
| 72 | create: (options) => | 166 | create: (options) => { |
| 73 | Effect.try({ | 167 | const webPreferences = options.webPreferences; |
| 168 | const diagnosticOptions = { | ||
| 169 | title: options.title ?? null, | ||
| 170 | width: options.width ?? null, | ||
| 171 | height: options.height ?? null, | ||
| 172 | minWidth: options.minWidth ?? null, | ||
| 173 | minHeight: options.minHeight ?? null, | ||
| 174 | show: options.show ?? null, | ||
| 175 | modal: options.modal ?? null, | ||
| 176 | frame: options.frame ?? null, | ||
| 177 | transparent: options.transparent ?? null, | ||
| 178 | backgroundColor: options.backgroundColor ?? null, | ||
| 179 | webPreferences: { | ||
| 180 | preload: webPreferences?.preload ?? null, | ||
| 181 | partition: webPreferences?.partition ?? null, | ||
| 182 | sandbox: webPreferences?.sandbox ?? null, | ||
| 183 | contextIsolation: webPreferences?.contextIsolation ?? null, | ||
| 184 | nodeIntegration: webPreferences?.nodeIntegration ?? null, | ||
| 185 | webviewTag: webPreferences?.webviewTag ?? null, | ||
| 186 | }, | ||
| 187 | } satisfies typeof ElectronWindowCreateOptions.Type; | ||
| 188 | |||
| 189 | return Effect.try({ | ||
| 74 | try: () => new Electron.BrowserWindow(options), | 190 | try: () => new Electron.BrowserWindow(options), |
| 75 | catch: (cause) => new ElectronWindowCreateError({ cause }), | 191 | catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }), |
| 76 | }), | 192 | }); |
| 193 | }, | ||
| 77 | main: liveMain, | 194 | main: liveMain, |
| 78 | currentMainOrFirst, | 195 | currentMainOrFirst, |
| 79 | focusedMainOrFirst, | 196 | focusedMainOrFirst, |
| ... | @@ -89,45 +206,75 @@ export const make = Effect.gen(function* () { | ... | @@ -89,45 +206,75 @@ export const make = Effect.gen(function* () { |
| 89 | return Option.none(); | 206 | return Option.none(); |
| 90 | }), | 207 | }), |
| 91 | reveal: (window) => | 208 | reveal: (window) => |
| 92 | Effect.sync(() => { | 209 | Effect.try({ |
| 93 | if (window.isDestroyed()) { | 210 | try: () => { |
| 94 | return; | 211 | if (window.isDestroyed()) { |
| 95 | } | 212 | return; |
| 213 | } | ||
| 96 | 214 | ||
| 97 | if (window.isMinimized()) { | 215 | if (window.isMinimized()) { |
| 98 | window.restore(); | 216 | window.restore(); |
| 99 | } | 217 | } |
| 100 | 218 | ||
| 101 | if (!window.isVisible()) { | 219 | if (!window.isVisible()) { |
| 102 | window.show(); | 220 | window.show(); |
| 103 | } | 221 | } |
| 104 | 222 | ||
| 105 | if (platform === "darwin") { | 223 | if (platform === "darwin") { |
| 106 | Electron.app.focus({ steal: true }); | 224 | Electron.app.focus({ steal: true }); |
| 107 | } | 225 | } |
| 108 | 226 | ||
| 109 | window.focus(); | 227 | window.focus(); |
| 110 | }), | 228 | }, |
| 229 | catch: (cause) => | ||
| 230 | new ElectronWindowOperationError({ | ||
| 231 | operation: "reveal-window", | ||
| 232 | platform, | ||
| 233 | windowId: window.id, | ||
| 234 | channel: null, | ||
| 235 | cause, | ||
| 236 | }), | ||
| 237 | }).pipe(Effect.orDie), | ||
| 111 | sendAll: (channel, ...args) => | 238 | sendAll: (channel, ...args) => |
| 112 | Effect.sync(() => { | 239 | Effect.gen(function* () { |
| 113 | for (const window of Electron.BrowserWindow.getAllWindows()) { | 240 | for (const window of yield* listWindows) { |
| 114 | if (window.isDestroyed()) { | 241 | if (yield* isWindowDestroyed(window)) { |
| 115 | continue; | 242 | continue; |
| 116 | } | 243 | } |
| 117 | window.webContents.send(channel, ...args); | 244 | yield* Effect.try({ |
| 245 | try: () => window.webContents.send(channel, ...args), | ||
| 246 | catch: (cause) => | ||
| 247 | new ElectronWindowOperationError({ | ||
| 248 | operation: "send-window-message", | ||
| 249 | platform, | ||
| 250 | windowId: window.id, | ||
| 251 | channel, | ||
| 252 | cause, | ||
| 253 | }), | ||
| 254 | }).pipe(Effect.orDie); | ||
| 118 | } | 255 | } |
| 119 | }), | 256 | }), |
| 120 | destroyAll: Effect.sync(() => { | 257 | destroyAll: Effect.gen(function* () { |
| 121 | for (const window of Electron.BrowserWindow.getAllWindows()) { | 258 | for (const window of yield* listWindows) { |
| 122 | window.destroy(); | 259 | yield* Effect.try({ |
| 260 | try: () => window.destroy(), | ||
| 261 | catch: (cause) => | ||
| 262 | new ElectronWindowOperationError({ | ||
| 263 | operation: "destroy-window", | ||
| 264 | platform, | ||
| 265 | windowId: window.id, | ||
| 266 | channel: null, | ||
| 267 | cause, | ||
| 268 | }), | ||
| 269 | }).pipe(Effect.orDie); | ||
| 123 | } | 270 | } |
| 124 | }), | 271 | }), |
| 125 | syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>( | 272 | syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>( |
| 126 | sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, | 273 | sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, |
| 127 | ) { | 274 | ) { |
| 128 | const windows = Electron.BrowserWindow.getAllWindows(); | 275 | const windows = yield* listWindows; |
| 129 | for (const window of windows) { | 276 | for (const window of windows) { |
| 130 | if (window.isDestroyed()) { | 277 | if (yield* isWindowDestroyed(window)) { |
| 131 | continue; | 278 | continue; |
| 132 | } | 279 | } |
| 133 | yield* sync(window); | 280 | yield* sync(window); |