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 type * as Electron from "electron"; import * as DesktopAssets from "../app/DesktopAssets.ts";import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import { makeComponentLogger } from "../app/DesktopObservability.ts";import * as DesktopState from "../app/DesktopState.ts";import * as ElectronMenu from "../electron/ElectronMenu.ts";import { getDesktopUrl } from "../electron/ElectronProtocol.ts";import * as ElectronShell from "../electron/ElectronShell.ts";import * as ElectronTheme from "../electron/ElectronTheme.ts";import * as ElectronWindow from "../electron/ElectronWindow.ts";import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";import * as PreviewManager from "../preview/Manager.ts"; const TITLEBAR_HEIGHT = 40;const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linuxconst TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; type WindowTitleBarOptions = Pick< Electron.BrowserWindowConstructorOptions, "titleBarOverlay" | "titleBarStyle" | "trafficLightPosition">; type DesktopWindowRuntimeServices = | DesktopEnvironment.DesktopEnvironment | DesktopAssets.DesktopAssets | DesktopState.DesktopState | ElectronMenu.ElectronMenu | ElectronShell.ElectronShell | ElectronTheme.ElectronTheme | ElectronWindow.ElectronWindow | PreviewManager.PreviewManager; export type DesktopWindowError = | ElectronWindow.ElectronWindowCreateError | PreviewManager.PreviewManagerError; export class DesktopWindow extends Context.Service< DesktopWindow, { readonly createMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly ensureMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly revealOrCreateMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>; readonly activate: Effect.Effect<void, DesktopWindowError>; readonly createMainIfBackendReady: Effect.Effect<void, DesktopWindowError>; readonly handleBackendReady: Effect.Effect<void, DesktopWindowError>; readonly dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>; readonly syncAppearance: Effect.Effect<void>; }>()("@t3tools/desktop/window/DesktopWindow") {} const { logInfo: logWindowInfo, logWarning: logWindowWarning } = makeComponentLogger("desktop-window"); function getIconOption( iconPaths: DesktopAssets.DesktopIconPaths, platform: NodeJS.Platform,): { icon: string } | Record<string, never> { if (platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = platform === "win32" ? "ico" : "png"; return Option.match(iconPaths[ext], { onNone: () => ({}), onSome: (icon) => ({ icon }), });} function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";} export function isSameOriginRendererNavigation(input: { readonly applicationUrl: string; readonly navigationUrl: string;}): boolean { try { return new URL(input.applicationUrl).origin === new URL(input.navigationUrl).origin; } catch { return false; }} function getWindowTitleBarOptions( shouldUseDarkColors: boolean, platform: NodeJS.Platform,): WindowTitleBarOptions { if (platform === "darwin") { return { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, }; } return { titleBarStyle: "hidden", titleBarOverlay: { color: TITLEBAR_COLOR, height: TITLEBAR_HEIGHT, symbolColor: shouldUseDarkColors ? TITLEBAR_DARK_SYMBOL_COLOR : TITLEBAR_LIGHT_SYMBOL_COLOR, }, };} function syncWindowAppearance( window: Electron.BrowserWindow, shouldUseDarkColors: boolean, platform: NodeJS.Platform,): Effect.Effect<void> { return Effect.sync(() => { if (window.isDestroyed()) { return; } window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform); if (typeof titleBarOverlay === "object") { window.setTitleBarOverlay(titleBarOverlay); } });} type RevealSubscription = (listener: () => void) => void; function bindFirstRevealTrigger( subscribers: readonly RevealSubscription[], reveal: () => void,): void { let revealed = false; const fire = () => { if (revealed) return; revealed = true; reveal(); }; for (const subscribe of subscribers) { subscribe(fire); }} export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const assets = yield* DesktopAssets.DesktopAssets; const electronMenu = yield* ElectronMenu.ElectronMenu; const electronShell = yield* ElectronShell.ElectronShell; const electronTheme = yield* ElectronTheme.ElectronTheme; const electronWindow = yield* ElectronWindow.ElectronWindow; const previewManager = yield* PreviewManager.PreviewManager; const state = yield* DesktopState.DesktopState; const context = yield* Effect.context<DesktopWindowRuntimeServices>(); const runPromise = Effect.runPromiseWith(context); const createWindow = Effect.fn("desktop.window.createWindow")(function* (): Effect.fn.Return< Electron.BrowserWindow, DesktopWindowError > { yield* previewManager.getBrowserSession(); const applicationUrl = getDesktopUrl(environment.isDevelopment); const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; const window = yield* electronWindow.create({ width: 1100, height: 780, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors), ...iconOption, title: environment.displayName, ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform), webPreferences: { preload: environment.preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, }, }); if (environment.platform === "darwin") { window.setAutoHideCursor(false); } yield* previewManager.setMainWindow(window); window.webContents.on("will-attach-webview", (event, webPreferences, params) => { if ( typeof params.partition !== "string" || !previewManager.isBrowserPartition(params.partition) ) { event.preventDefault(); return; } webPreferences.sandbox = true; webPreferences.nodeIntegration = false; webPreferences.nodeIntegrationInSubFrames = false; webPreferences.contextIsolation = false; }); window.webContents.on("context-menu", (event, params) => { event.preventDefault(); const menuTemplate: Electron.MenuItemConstructorOptions[] = []; if (params.misspelledWord) { for (const suggestion of params.dictionarySuggestions.slice(0, 5)) { menuTemplate.push({ label: suggestion, click: () => window.webContents.replaceMisspelling(suggestion), }); } if (params.dictionarySuggestions.length === 0) { menuTemplate.push({ label: "No suggestions", enabled: false }); } menuTemplate.push({ type: "separator" }); } if (Option.isSome(ElectronShell.parseSafeExternalUrl(params.linkURL))) { menuTemplate.push( { label: "Copy Link", click: () => { void runPromise( electronShell.copyText(params.linkURL).pipe(Effect.ignore({ log: true })), ); }, }, { type: "separator" }, ); } if (params.mediaType === "image") { menuTemplate.push({ label: "Copy Image", click: () => window.webContents.copyImageAt(params.x, params.y), }); menuTemplate.push({ type: "separator" }); } menuTemplate.push( { role: "cut", enabled: params.editFlags.canCut }, { role: "copy", enabled: params.editFlags.canCopy }, { role: "paste", enabled: params.editFlags.canPaste }, { role: "selectAll", enabled: params.editFlags.canSelectAll }, ); void runPromise(electronMenu.popupTemplate({ window, template: menuTemplate })); }); window.webContents.setWindowOpenHandler(({ url }) => { if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url).pipe(Effect.ignore({ log: true }))); } return { action: "deny" }; }); window.webContents.on("will-navigate", (event, url) => { if ( isSameOriginRendererNavigation({ applicationUrl, navigationUrl: url, }) ) { return; } event.preventDefault(); if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) { void runPromise(electronShell.openExternal(url).pipe(Effect.ignore({ log: true }))); } }); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(environment.displayName); }); window.webContents.on("did-finish-load", () => { window.setTitle(environment.displayName); }); window.webContents.on( "did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame) { return; } void runPromise( logWindowWarning("main window failed to load", { errorCode, errorDescription, url: validatedURL, }), ); }, ); window.webContents.on("render-process-gone", (_event, details) => { void runPromise( logWindowWarning("main window render process gone", { reason: details.reason, exitCode: details.exitCode, }), ); }); const revealSubscribers: RevealSubscription[] = [(fire) => window.once("ready-to-show", fire)]; if (environment.platform === "linux") { revealSubscribers.push((fire) => window.webContents.once("did-finish-load", fire)); } bindFirstRevealTrigger(revealSubscribers, () => { void runPromise(electronWindow.reveal(window)); }); if (environment.isDevelopment) { void window.loadURL(applicationUrl); window.webContents.openDevTools({ mode: "detach" }); } else { void window.loadURL(applicationUrl); } window.on("closed", () => { void runPromise(electronWindow.clearMain(Option.some(window))); }); return window; }); const createMain = Effect.gen(function* () { const window = yield* createWindow(); yield* electronWindow.setMain(window); yield* logWindowInfo("main window created"); return window; }).pipe(Effect.withSpan("desktop.window.createMain")); const ensureMain = Effect.gen(function* () { const existingWindow = yield* electronWindow.currentMainOrFirst; if (Option.isSome(existingWindow)) { return existingWindow.value; } return yield* createMain; }).pipe(Effect.withSpan("desktop.window.ensureMain")); const revealOrCreateMain = Effect.gen(function* () { const window = yield* ensureMain; yield* electronWindow.reveal(window); return window; }).pipe(Effect.withSpan("desktop.window.revealOrCreateMain")); const createMainIfBackendReady = Effect.gen(function* () { const backendReady = yield* Ref.get(state.backendReady); if (!backendReady) return; const existingWindow = yield* electronWindow.currentMainOrFirst; if (Option.isSome(existingWindow)) return; yield* createMain; }).pipe(Effect.withSpan("desktop.window.createMainIfBackendReady")); return DesktopWindow.of({ createMain, ensureMain, revealOrCreateMain, activate: Effect.gen(function* () { const existingWindow = yield* electronWindow.currentMainOrFirst; if (Option.isSome(existingWindow)) { yield* electronWindow.reveal(existingWindow.value); } else { yield* createMainIfBackendReady; } }).pipe(Effect.withSpan("desktop.window.activate")), createMainIfBackendReady, handleBackendReady: Effect.gen(function* () { yield* Ref.set(state.backendReady, true); yield* logWindowInfo("backend ready", { source: "http" }); yield* createMainIfBackendReady; }).pipe(Effect.withSpan("desktop.window.handleBackendReady")), dispatchMenuAction: Effect.fn("desktop.window.dispatchMenuAction")(function* (action) { yield* Effect.annotateCurrentSpan({ action }); const existingWindow = yield* electronWindow.focusedMainOrFirst; const targetWindow = Option.isSome(existingWindow) ? existingWindow.value : yield* createMain; const send = () => { if (targetWindow.isDestroyed()) return; targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); void runPromise(electronWindow.reveal(targetWindow)); }; if (targetWindow.webContents.isLoadingMainFrame()) { targetWindow.webContents.once("did-finish-load", send); return; } send(); }), syncAppearance: Effect.gen(function* () { const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; yield* electronWindow.syncAllAppearance((window) => syncWindowAppearance(window, shouldUseDarkColors, environment.platform), ); }).pipe(Effect.withSpan("desktop.window.syncAppearance")), });}); export const layer = Layer.effect(DesktopWindow, make);
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 type * as Electron from "electron";
import * as DesktopAssets from "../app/DesktopAssets.ts";
import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import { makeComponentLogger } from "../app/DesktopObservability.ts";
import * as DesktopState from "../app/DesktopState.ts";
import * as ElectronMenu from "../electron/ElectronMenu.ts";
import { getDesktopUrl } from "../electron/ElectronProtocol.ts";
import * as ElectronShell from "../electron/ElectronShell.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";
import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";
import * as PreviewManager from "../preview/Manager.ts";
const TITLEBAR_HEIGHT = 40;
const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937";
const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc";
type WindowTitleBarOptions = Pick<
Electron.BrowserWindowConstructorOptions,
"titleBarOverlay" | "titleBarStyle" | "trafficLightPosition"
>;
type DesktopWindowRuntimeServices =
| DesktopEnvironment.DesktopEnvironment
| DesktopAssets.DesktopAssets
| DesktopState.DesktopState
| ElectronMenu.ElectronMenu
| ElectronShell.ElectronShell
| ElectronTheme.ElectronTheme
| ElectronWindow.ElectronWindow
| PreviewManager.PreviewManager;
export type DesktopWindowError =
| ElectronWindow.ElectronWindowCreateError
| PreviewManager.PreviewManagerError;
export class DesktopWindow extends Context.Service<
DesktopWindow,
{
readonly createMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>;
readonly ensureMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>;
readonly revealOrCreateMain: Effect.Effect<Electron.BrowserWindow, DesktopWindowError>;
readonly activate: Effect.Effect<void, DesktopWindowError>;
readonly createMainIfBackendReady: Effect.Effect<void, DesktopWindowError>;
readonly handleBackendReady: Effect.Effect<void, DesktopWindowError>;
readonly dispatchMenuAction: (action: string) => Effect.Effect<void, DesktopWindowError>;
readonly syncAppearance: Effect.Effect<void>;
}
>()("@t3tools/desktop/window/DesktopWindow") {}
const { logInfo: logWindowInfo, logWarning: logWindowWarning } =
makeComponentLogger("desktop-window");
function getIconOption(
iconPaths: DesktopAssets.DesktopIconPaths,
platform: NodeJS.Platform,
): { icon: string } | Record<string, never> {
if (platform === "darwin") return {}; // macOS uses .icns from app bundle
const ext = platform === "win32" ? "ico" : "png";
return Option.match(iconPaths[ext], {
onNone: () => ({}),
onSome: (icon) => ({ icon }),
});
}
function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string {
return shouldUseDarkColors ? "#0a0a0a" : "#ffffff";
}
export function isSameOriginRendererNavigation(input: {
readonly applicationUrl: string;
readonly navigationUrl: string;
}): boolean {
try {
return new URL(input.applicationUrl).origin === new URL(input.navigationUrl).origin;
} catch {
return false;
}
}
function getWindowTitleBarOptions(
shouldUseDarkColors: boolean,
platform: NodeJS.Platform,
): WindowTitleBarOptions {
if (platform === "darwin") {
return {
titleBarStyle: "hiddenInset",
trafficLightPosition: { x: 16, y: 18 },
};
}
return {
titleBarStyle: "hidden",
titleBarOverlay: {
color: TITLEBAR_COLOR,
height: TITLEBAR_HEIGHT,
symbolColor: shouldUseDarkColors ? TITLEBAR_DARK_SYMBOL_COLOR : TITLEBAR_LIGHT_SYMBOL_COLOR,
},
};
}
function syncWindowAppearance(
window: Electron.BrowserWindow,
shouldUseDarkColors: boolean,
platform: NodeJS.Platform,
): Effect.Effect<void> {
return Effect.sync(() => {
if (window.isDestroyed()) {
return;
}
window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors));
const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform);
if (typeof titleBarOverlay === "object") {
window.setTitleBarOverlay(titleBarOverlay);
}
});
}
type RevealSubscription = (listener: () => void) => void;
function bindFirstRevealTrigger(
subscribers: readonly RevealSubscription[],
reveal: () => void,
): void {
let revealed = false;
const fire = () => {
if (revealed) return;
revealed = true;
reveal();
};
for (const subscribe of subscribers) {
subscribe(fire);
}
}
export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const assets = yield* DesktopAssets.DesktopAssets;
const electronMenu = yield* ElectronMenu.ElectronMenu;
const electronShell = yield* ElectronShell.ElectronShell;
const electronTheme = yield* ElectronTheme.ElectronTheme;
const electronWindow = yield* ElectronWindow.ElectronWindow;
const previewManager = yield* PreviewManager.PreviewManager;
const state = yield* DesktopState.DesktopState;
const context = yield* Effect.context<DesktopWindowRuntimeServices>();
const runPromise = Effect.runPromiseWith(context);
const createWindow = Effect.fn("desktop.window.createWindow")(function* (): Effect.fn.Return<
Electron.BrowserWindow,
DesktopWindowError
> {
yield* previewManager.getBrowserSession();
const applicationUrl = getDesktopUrl(environment.isDevelopment);
const iconPaths = yield* assets.iconPaths;
const iconOption = getIconOption(iconPaths, environment.platform);
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
const window = yield* electronWindow.create({
width: 1100,
height: 780,
minWidth: 840,
minHeight: 620,
show: false,
autoHideMenuBar: true,
...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}),
backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors),
...iconOption,
title: environment.displayName,
...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform),
webPreferences: {
preload: environment.preloadPath,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webviewTag: true,
},
});
if (environment.platform === "darwin") {
window.setAutoHideCursor(false);
}
yield* previewManager.setMainWindow(window);
window.webContents.on("will-attach-webview", (event, webPreferences, params) => {
if (
typeof params.partition !== "string" ||
!previewManager.isBrowserPartition(params.partition)
) {
event.preventDefault();
return;
}
webPreferences.sandbox = true;
webPreferences.nodeIntegration = false;
webPreferences.nodeIntegrationInSubFrames = false;
webPreferences.contextIsolation = false;
});
window.webContents.on("context-menu", (event, params) => {
event.preventDefault();
const menuTemplate: Electron.MenuItemConstructorOptions[] = [];
if (params.misspelledWord) {
for (const suggestion of params.dictionarySuggestions.slice(0, 5)) {
menuTemplate.push({
label: suggestion,
click: () => window.webContents.replaceMisspelling(suggestion),
});
}
if (params.dictionarySuggestions.length === 0) {
menuTemplate.push({ label: "No suggestions", enabled: false });
}
menuTemplate.push({ type: "separator" });
}
if (Option.isSome(ElectronShell.parseSafeExternalUrl(params.linkURL))) {
menuTemplate.push(
{
label: "Copy Link",
click: () => {
void runPromise(
electronShell.copyText(params.linkURL).pipe(Effect.ignore({ log: true })),
);
},
},
{ type: "separator" },
);
}
if (params.mediaType === "image") {
menuTemplate.push({
label: "Copy Image",
click: () => window.webContents.copyImageAt(params.x, params.y),
});
menuTemplate.push({ type: "separator" });
}
menuTemplate.push(
{ role: "cut", enabled: params.editFlags.canCut },
{ role: "copy", enabled: params.editFlags.canCopy },
{ role: "paste", enabled: params.editFlags.canPaste },
{ role: "selectAll", enabled: params.editFlags.canSelectAll },
);
void runPromise(electronMenu.popupTemplate({ window, template: menuTemplate }));
});
window.webContents.setWindowOpenHandler(({ url }) => {
if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) {
void runPromise(electronShell.openExternal(url).pipe(Effect.ignore({ log: true })));
}
return { action: "deny" };
});
window.webContents.on("will-navigate", (event, url) => {
if (
isSameOriginRendererNavigation({
applicationUrl,
navigationUrl: url,
})
) {
return;
}
event.preventDefault();
if (Option.isSome(ElectronShell.parseSafeExternalUrl(url))) {
void runPromise(electronShell.openExternal(url).pipe(Effect.ignore({ log: true })));
}
});
window.on("page-title-updated", (event) => {
event.preventDefault();
window.setTitle(environment.displayName);
});
window.webContents.on("did-finish-load", () => {
window.setTitle(environment.displayName);
});
window.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!isMainFrame) {
return;
}
void runPromise(
logWindowWarning("main window failed to load", {
errorCode,
errorDescription,
url: validatedURL,
}),
);
},
);
window.webContents.on("render-process-gone", (_event, details) => {
void runPromise(
logWindowWarning("main window render process gone", {
reason: details.reason,
exitCode: details.exitCode,
}),
);
});
const revealSubscribers: RevealSubscription[] = [(fire) => window.once("ready-to-show", fire)];
if (environment.platform === "linux") {
revealSubscribers.push((fire) => window.webContents.once("did-finish-load", fire));
}
bindFirstRevealTrigger(revealSubscribers, () => {
void runPromise(electronWindow.reveal(window));
});
if (environment.isDevelopment) {
void window.loadURL(applicationUrl);
window.webContents.openDevTools({ mode: "detach" });
} else {
void window.loadURL(applicationUrl);
}
window.on("closed", () => {
void runPromise(electronWindow.clearMain(Option.some(window)));
});
return window;
});
const createMain = Effect.gen(function* () {
const window = yield* createWindow();
yield* electronWindow.setMain(window);
yield* logWindowInfo("main window created");
return window;
}).pipe(Effect.withSpan("desktop.window.createMain"));
const ensureMain = Effect.gen(function* () {
const existingWindow = yield* electronWindow.currentMainOrFirst;
if (Option.isSome(existingWindow)) {
return existingWindow.value;
}
return yield* createMain;
}).pipe(Effect.withSpan("desktop.window.ensureMain"));
const revealOrCreateMain = Effect.gen(function* () {
const window = yield* ensureMain;
yield* electronWindow.reveal(window);
return window;
}).pipe(Effect.withSpan("desktop.window.revealOrCreateMain"));
const createMainIfBackendReady = Effect.gen(function* () {
const backendReady = yield* Ref.get(state.backendReady);
if (!backendReady) return;
const existingWindow = yield* electronWindow.currentMainOrFirst;
if (Option.isSome(existingWindow)) return;
yield* createMain;
}).pipe(Effect.withSpan("desktop.window.createMainIfBackendReady"));
return DesktopWindow.of({
createMain,
ensureMain,
revealOrCreateMain,
activate: Effect.gen(function* () {
const existingWindow = yield* electronWindow.currentMainOrFirst;
if (Option.isSome(existingWindow)) {
yield* electronWindow.reveal(existingWindow.value);
} else {
yield* createMainIfBackendReady;
}
}).pipe(Effect.withSpan("desktop.window.activate")),
createMainIfBackendReady,
handleBackendReady: Effect.gen(function* () {
yield* Ref.set(state.backendReady, true);
yield* logWindowInfo("backend ready", { source: "http" });
yield* createMainIfBackendReady;
}).pipe(Effect.withSpan("desktop.window.handleBackendReady")),
dispatchMenuAction: Effect.fn("desktop.window.dispatchMenuAction")(function* (action) {
yield* Effect.annotateCurrentSpan({ action });
const existingWindow = yield* electronWindow.focusedMainOrFirst;
const targetWindow = Option.isSome(existingWindow) ? existingWindow.value : yield* createMain;
const send = () => {
if (targetWindow.isDestroyed()) return;
targetWindow.webContents.send(MENU_ACTION_CHANNEL, action);
void runPromise(electronWindow.reveal(targetWindow));
};
if (targetWindow.webContents.isLoadingMainFrame()) {
targetWindow.webContents.once("did-finish-load", send);
return;
}
send();
}),
syncAppearance: Effect.gen(function* () {
const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors;
yield* electronWindow.syncAllAppearance((window) =>
syncWindowAppearance(window, shouldUseDarkColors, environment.platform),
);
}).pipe(Effect.withSpan("desktop.window.syncAppearance")),
});
});
export const layer = Layer.effect(DesktopWindow, make);