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 Schema from "effect/Schema"; import type * as Electron from "electron"; import { makeComponentLogger } from "../app/DesktopObservability.ts"; import * as ElectronApp from "../electron/ElectronApp.ts"; import * as ElectronDialog from "../electron/ElectronDialog.ts"; import * as ElectronMenu from "../electron/ElectronMenu.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; import * as DesktopWindow from "./DesktopWindow.ts"; export class DesktopApplicationMenuActionError extends Schema.TaggedErrorClass()( "DesktopApplicationMenuActionError", { action: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Desktop menu action "${this.action}" failed.`; } } export class DesktopApplicationMenu extends Context.Service< DesktopApplicationMenu, { readonly configure: Effect.Effect; } >()("@t3tools/desktop/window/DesktopApplicationMenu") {} type DesktopApplicationMenuRuntimeServices = | DesktopUpdates.DesktopUpdates | DesktopWindow.DesktopWindow | ElectronDialog.ElectronDialog; const { logInfo: logUpdaterInfo } = makeComponentLogger("desktop-updater"); const { logError: logMenuError } = makeComponentLogger("desktop-menu"); const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* ( action: string, ): Effect.fn.Return { const desktopWindow = yield* DesktopWindow.DesktopWindow; yield* desktopWindow.dispatchMenuAction(action); }); const checkForUpdatesFromMenu = Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; const electronDialog = yield* ElectronDialog.ElectronDialog; const result = yield* updates.check("menu"); const updateState = result.state; if (updateState.status === "up-to-date") { yield* electronDialog.showMessageBox({ type: "info", title: "You're up to date!", message: `T3 Code ${updateState.currentVersion} is currently the newest version available.`, buttons: ["OK"], }); } else if (updateState.status === "error") { yield* electronDialog.showMessageBox({ type: "warning", title: "Update check failed", message: "Could not check for updates.", detail: updateState.message ?? "An unknown error occurred. Please try again later.", buttons: ["OK"], }); } }).pipe(Effect.withSpan("desktop.menu.checkForUpdates")); const handleCheckForUpdatesMenuClick = Effect.gen(function* () { const updates = yield* DesktopUpdates.DesktopUpdates; const electronDialog = yield* ElectronDialog.ElectronDialog; const disabledReason = yield* updates.disabledReason; if (Option.isSome(disabledReason)) { yield* logUpdaterInfo("manual update check requested, but updates are disabled", { disabledReason: disabledReason.value, }); yield* electronDialog.showMessageBox({ type: "info", title: "Updates unavailable", message: "Automatic updates are not available right now.", detail: disabledReason.value, buttons: ["OK"], }); return; } const desktopWindow = yield* DesktopWindow.DesktopWindow; yield* desktopWindow.ensureMain; yield* checkForUpdatesFromMenu; }).pipe(Effect.withSpan("desktop.menu.handleCheckForUpdatesClick")); export const make = Effect.gen(function* () { const electronApp = yield* ElectronApp.ElectronApp; const electronMenu = yield* ElectronMenu.ElectronMenu; const environment = yield* DesktopEnvironment.DesktopEnvironment; const appName = yield* electronApp.name; const context = yield* Effect.context(); const runPromise = Effect.runPromiseWith(context); const runMenuEffect = ( action: string, effect: Effect.Effect, ) => { void runPromise( effect.pipe( Effect.annotateLogs({ action }), Effect.withSpan("desktop.menu.action"), Effect.catchCause((cause) => { const error = new DesktopApplicationMenuActionError({ action, cause }); return logMenuError(error.message, { error }); }), ), ); }; const configure = Effect.gen(function* () { const checkForUpdatesClick = () => { runMenuEffect("check-for-updates", handleCheckForUpdatesMenuClick); }; const settingsClick = () => { runMenuEffect("open-settings", dispatchMenuAction("open-settings")); }; const template: Electron.MenuItemConstructorOptions[] = []; if (environment.platform === "darwin") { template.push({ label: appName, submenu: [ { role: "about" }, { label: "Check for Updates...", click: checkForUpdatesClick, }, { type: "separator" }, { label: "Settings...", accelerator: "CmdOrCtrl+,", click: settingsClick, }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }); } template.push( { label: "File", submenu: [ ...(environment.platform === "darwin" ? [] : [ { label: "Settings...", accelerator: "CmdOrCtrl+,", click: settingsClick, }, { type: "separator" as const }, ]), { role: environment.platform === "darwin" ? "close" : "quit" }, ], }, { role: "editMenu" }, { label: "View", submenu: [ { role: "reload" }, { role: "forceReload" }, { role: "toggleDevTools" }, { type: "separator" }, { role: "resetZoom" }, { role: "zoomIn", accelerator: "CmdOrCtrl+=" }, { role: "zoomIn", accelerator: "CmdOrCtrl+Plus", visible: false }, { role: "zoomOut" }, { type: "separator" }, { role: "togglefullscreen" }, ], }, { role: "windowMenu" }, { role: "help", submenu: [ { label: "Check for Updates...", click: checkForUpdatesClick, }, ], }, ); yield* electronMenu.setApplicationMenu(template); }).pipe(Effect.withSpan("desktop.menu.configure")); return DesktopApplicationMenu.of({ configure, }); }); export const layer = Layer.effect(DesktopApplicationMenu, make);