import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema"; import * as Electron from "electron"; const ElectronWindowCreateOptions = Schema.Struct({ title: Schema.NullOr(Schema.String), width: Schema.NullOr(Schema.Number), height: Schema.NullOr(Schema.Number), minWidth: Schema.NullOr(Schema.Number), minHeight: Schema.NullOr(Schema.Number), show: Schema.NullOr(Schema.Boolean), modal: Schema.NullOr(Schema.Boolean), frame: Schema.NullOr(Schema.Boolean), transparent: Schema.NullOr(Schema.Boolean), backgroundColor: Schema.NullOr(Schema.String), webPreferences: Schema.Struct({ preload: Schema.NullOr(Schema.String), partition: Schema.NullOr(Schema.String), backgroundThrottling: Schema.NullOr(Schema.Boolean), sandbox: Schema.NullOr(Schema.Boolean), contextIsolation: Schema.NullOr(Schema.Boolean), nodeIntegration: Schema.NullOr(Schema.Boolean), webviewTag: Schema.NullOr(Schema.Boolean), }),}); const ElectronWindowOperation = Schema.Literals([ "list-windows", "get-focused-window", "inspect-window", "reveal-window", "send-window-message", "destroy-window",]); export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()( "ElectronWindowCreateError", { options: ElectronWindowCreateOptions, cause: Schema.Defect(), },) { override get message(): string { const title = this.options.title === null ? "" : ` "${this.options.title}"`; const dimensions = this.options.width === null || this.options.height === null ? "" : ` (${this.options.width}x${this.options.height})`; return `Failed to create Electron BrowserWindow${title}${dimensions}.`; }} export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError); export class ElectronWindowOperationError extends Schema.TaggedErrorClass<ElectronWindowOperationError>()( "ElectronWindowOperationError", { operation: ElectronWindowOperation, platform: Schema.String, windowId: Schema.NullOr(Schema.Number), channel: Schema.NullOr(Schema.String), cause: Schema.Defect(), },) { override get message(): string { const window = this.windowId === null ? "" : ` for window ${this.windowId}`; const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`; return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`; }} export class ElectronWindow extends Context.Service< ElectronWindow, { readonly create: ( options: Electron.BrowserWindowConstructorOptions, ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>; readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>; readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>; readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>; readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>; readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>; readonly destroyAll: Effect.Effect<void>; readonly syncAllAppearance: <E, R>( sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, ) => Effect.Effect<void, E, R>; }>()("@t3tools/desktop/electron/ElectronWindow") {} export const make = Effect.gen(function* () { const platform = yield* HostProcessPlatform; const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); const listWindows = Effect.try({ try: () => Electron.BrowserWindow.getAllWindows(), catch: (cause) => new ElectronWindowOperationError({ operation: "list-windows", platform, windowId: null, channel: null, cause, }), }).pipe(Effect.orDie); const isWindowDestroyed = (window: Electron.BrowserWindow) => Effect.try({ try: () => window.isDestroyed(), catch: (cause) => new ElectronWindowOperationError({ operation: "inspect-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie); const liveMain = Effect.gen(function* () { const main = yield* Ref.get(mainWindowRef); if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) { return Option.none<Electron.BrowserWindow>(); } return main; }); const currentMainOrFirst = Effect.gen(function* () { const main = yield* liveMain; if (Option.isSome(main)) { return main; } const first = Option.fromNullishOr((yield* listWindows)[0] ?? null); if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) { return Option.none<Electron.BrowserWindow>(); } return first; }); const focusedMainOrFirst = Effect.gen(function* () { const focused = yield* Effect.try({ try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null), catch: (cause) => new ElectronWindowOperationError({ operation: "get-focused-window", platform, windowId: null, channel: null, cause, }), }).pipe(Effect.orDie); if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) { return focused; } return yield* currentMainOrFirst; }); return ElectronWindow.of({ create: (options) => { const webPreferences = options.webPreferences; const diagnosticOptions = { title: options.title ?? null, width: options.width ?? null, height: options.height ?? null, minWidth: options.minWidth ?? null, minHeight: options.minHeight ?? null, show: options.show ?? null, modal: options.modal ?? null, frame: options.frame ?? null, transparent: options.transparent ?? null, backgroundColor: options.backgroundColor ?? null, webPreferences: { preload: webPreferences?.preload ?? null, partition: webPreferences?.partition ?? null, backgroundThrottling: webPreferences?.backgroundThrottling ?? null, sandbox: webPreferences?.sandbox ?? null, contextIsolation: webPreferences?.contextIsolation ?? null, nodeIntegration: webPreferences?.nodeIntegration ?? null, webviewTag: webPreferences?.webviewTag ?? null, }, } satisfies typeof ElectronWindowCreateOptions.Type; return Effect.try({ try: () => new Electron.BrowserWindow(options), catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }), }); }, main: liveMain, currentMainOrFirst, focusedMainOrFirst, setMain: (window) => Ref.set(mainWindowRef, Option.some(window)), clearMain: (window) => Ref.update(mainWindowRef, (current) => { if (Option.isNone(current)) { return current; } if (Option.isSome(window) && current.value !== window.value) { return current; } return Option.none(); }), reveal: (window) => Effect.try({ try: () => { if (window.isDestroyed()) { return; } if (window.isMinimized()) { window.restore(); } if (!window.isVisible()) { window.show(); } if (platform === "darwin") { Electron.app.focus({ steal: true }); } window.focus(); }, catch: (cause) => new ElectronWindowOperationError({ operation: "reveal-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie), sendAll: (channel, ...args) => Effect.gen(function* () { for (const window of yield* listWindows) { if (yield* isWindowDestroyed(window)) { continue; } yield* Effect.try({ try: () => window.webContents.send(channel, ...args), catch: (cause) => new ElectronWindowOperationError({ operation: "send-window-message", platform, windowId: window.id, channel, cause, }), }).pipe(Effect.orDie); } }), destroyAll: Effect.gen(function* () { for (const window of yield* listWindows) { yield* Effect.try({ try: () => window.destroy(), catch: (cause) => new ElectronWindowOperationError({ operation: "destroy-window", platform, windowId: window.id, channel: null, cause, }), }).pipe(Effect.orDie); } }), syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>( sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, ) { const windows = yield* listWindows; for (const window of windows) { if (yield* isWindowDestroyed(window)) { continue; } yield* sync(window); } }), });}); export const layer = Layer.effect(ElectronWindow, make);
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as Electron from "electron";
const ElectronWindowCreateOptions = Schema.Struct({
title: Schema.NullOr(Schema.String),
width: Schema.NullOr(Schema.Number),
height: Schema.NullOr(Schema.Number),
minWidth: Schema.NullOr(Schema.Number),
minHeight: Schema.NullOr(Schema.Number),
show: Schema.NullOr(Schema.Boolean),
modal: Schema.NullOr(Schema.Boolean),
frame: Schema.NullOr(Schema.Boolean),
transparent: Schema.NullOr(Schema.Boolean),
backgroundColor: Schema.NullOr(Schema.String),
webPreferences: Schema.Struct({
preload: Schema.NullOr(Schema.String),
partition: Schema.NullOr(Schema.String),
backgroundThrottling: Schema.NullOr(Schema.Boolean),
sandbox: Schema.NullOr(Schema.Boolean),
contextIsolation: Schema.NullOr(Schema.Boolean),
nodeIntegration: Schema.NullOr(Schema.Boolean),
webviewTag: Schema.NullOr(Schema.Boolean),
}),
});
const ElectronWindowOperation = Schema.Literals([
"list-windows",
"get-focused-window",
"inspect-window",
"reveal-window",
"send-window-message",
"destroy-window",
]);
export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()(
"ElectronWindowCreateError",
{
options: ElectronWindowCreateOptions,
cause: Schema.Defect(),
},
) {
override get message(): string {
const title = this.options.title === null ? "" : ` "${this.options.title}"`;
const dimensions =
this.options.width === null || this.options.height === null
? ""
: ` (${this.options.width}x${this.options.height})`;
return `Failed to create Electron BrowserWindow${title}${dimensions}.`;
}
}
export const isElectronWindowCreateError = Schema.is(ElectronWindowCreateError);
export class ElectronWindowOperationError extends Schema.TaggedErrorClass<ElectronWindowOperationError>()(
"ElectronWindowOperationError",
{
operation: ElectronWindowOperation,
platform: Schema.String,
windowId: Schema.NullOr(Schema.Number),
channel: Schema.NullOr(Schema.String),
cause: Schema.Defect(),
},
) {
override get message(): string {
const window = this.windowId === null ? "" : ` for window ${this.windowId}`;
const channel = this.channel === null ? "" : ` on channel ${JSON.stringify(this.channel)}`;
return `Electron window operation ${JSON.stringify(this.operation)} failed${window}${channel} on ${this.platform}.`;
}
}
export class ElectronWindow extends Context.Service<
ElectronWindow,
{
readonly create: (
options: Electron.BrowserWindowConstructorOptions,
) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;
readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;
readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;
readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
readonly destroyAll: Effect.Effect<void>;
readonly syncAllAppearance: <E, R>(
sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
) => Effect.Effect<void, E, R>;
}
>()("@t3tools/desktop/electron/ElectronWindow") {}
export const make = Effect.gen(function* () {
const platform = yield* HostProcessPlatform;
const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const listWindows = Effect.try({
try: () => Electron.BrowserWindow.getAllWindows(),
catch: (cause) =>
new ElectronWindowOperationError({
operation: "list-windows",
platform,
windowId: null,
channel: null,
cause,
}),
}).pipe(Effect.orDie);
const isWindowDestroyed = (window: Electron.BrowserWindow) =>
Effect.try({
try: () => window.isDestroyed(),
catch: (cause) =>
new ElectronWindowOperationError({
operation: "inspect-window",
platform,
windowId: window.id,
channel: null,
cause,
}),
}).pipe(Effect.orDie);
const liveMain = Effect.gen(function* () {
const main = yield* Ref.get(mainWindowRef);
if (Option.isNone(main) || (yield* isWindowDestroyed(main.value))) {
return Option.none<Electron.BrowserWindow>();
}
return main;
});
const currentMainOrFirst = Effect.gen(function* () {
const main = yield* liveMain;
if (Option.isSome(main)) {
return main;
}
const first = Option.fromNullishOr((yield* listWindows)[0] ?? null);
if (Option.isNone(first) || (yield* isWindowDestroyed(first.value))) {
return Option.none<Electron.BrowserWindow>();
}
return first;
});
const focusedMainOrFirst = Effect.gen(function* () {
const focused = yield* Effect.try({
try: () => Option.fromNullishOr(Electron.BrowserWindow.getFocusedWindow() ?? null),
catch: (cause) =>
new ElectronWindowOperationError({
operation: "get-focused-window",
platform,
windowId: null,
channel: null,
cause,
}),
}).pipe(Effect.orDie);
if (Option.isSome(focused) && !(yield* isWindowDestroyed(focused.value))) {
return focused;
}
return yield* currentMainOrFirst;
});
return ElectronWindow.of({
create: (options) => {
const webPreferences = options.webPreferences;
const diagnosticOptions = {
title: options.title ?? null,
width: options.width ?? null,
height: options.height ?? null,
minWidth: options.minWidth ?? null,
minHeight: options.minHeight ?? null,
show: options.show ?? null,
modal: options.modal ?? null,
frame: options.frame ?? null,
transparent: options.transparent ?? null,
backgroundColor: options.backgroundColor ?? null,
webPreferences: {
preload: webPreferences?.preload ?? null,
partition: webPreferences?.partition ?? null,
backgroundThrottling: webPreferences?.backgroundThrottling ?? null,
sandbox: webPreferences?.sandbox ?? null,
contextIsolation: webPreferences?.contextIsolation ?? null,
nodeIntegration: webPreferences?.nodeIntegration ?? null,
webviewTag: webPreferences?.webviewTag ?? null,
},
} satisfies typeof ElectronWindowCreateOptions.Type;
return Effect.try({
try: () => new Electron.BrowserWindow(options),
catch: (cause) => new ElectronWindowCreateError({ options: diagnosticOptions, cause }),
});
},
main: liveMain,
currentMainOrFirst,
focusedMainOrFirst,
setMain: (window) => Ref.set(mainWindowRef, Option.some(window)),
clearMain: (window) =>
Ref.update(mainWindowRef, (current) => {
if (Option.isNone(current)) {
return current;
}
if (Option.isSome(window) && current.value !== window.value) {
return current;
}
return Option.none();
}),
reveal: (window) =>
Effect.try({
try: () => {
if (window.isDestroyed()) {
return;
}
if (window.isMinimized()) {
window.restore();
}
if (!window.isVisible()) {
window.show();
}
if (platform === "darwin") {
Electron.app.focus({ steal: true });
}
window.focus();
},
catch: (cause) =>
new ElectronWindowOperationError({
operation: "reveal-window",
platform,
windowId: window.id,
channel: null,
cause,
}),
}).pipe(Effect.orDie),
sendAll: (channel, ...args) =>
Effect.gen(function* () {
for (const window of yield* listWindows) {
if (yield* isWindowDestroyed(window)) {
continue;
}
yield* Effect.try({
try: () => window.webContents.send(channel, ...args),
catch: (cause) =>
new ElectronWindowOperationError({
operation: "send-window-message",
platform,
windowId: window.id,
channel,
cause,
}),
}).pipe(Effect.orDie);
}
}),
destroyAll: Effect.gen(function* () {
for (const window of yield* listWindows) {
yield* Effect.try({
try: () => window.destroy(),
catch: (cause) =>
new ElectronWindowOperationError({
operation: "destroy-window",
platform,
windowId: window.id,
channel: null,
cause,
}),
}).pipe(Effect.orDie);
}
}),
syncAllAppearance: Effect.fn("desktop.electron.window.syncAllAppearance")(function* <E, R>(
sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
) {
const windows = yield* listWindows;
for (const window of windows) {
if (yield* isWindowDestroyed(window)) {
continue;
}
yield* sync(window);
}
}),
});
});
export const layer = Layer.effect(ElectronWindow, make);