| author | |
| committer | |
| log | b19fc1b87b22a3c923e60c2ef2a1dae336279924 |
| tree | 409474b8aa0d7d1a750c0a7edf1174ac9237f11f |
| parent | 2fa37ec183e0b99ca2ea77efbcb615f11019da75 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>21 files changed, 351 insertions(+), 353 deletions(-)
apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts-3| ... | ... | @@ -237,7 +237,6 @@ describe("DesktopConnectionCatalogStore", () => { |
| 237 | 237 | error, |
| 238 | 238 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError, |
| 239 | 239 | ); |
| 240 | assert.equal(error.operation, "decode-catalog-document"); | |
| 241 | 240 | assert.equal(error.catalogPath, catalogPath); |
| 242 | 241 | assert.exists(error.cause); |
| 243 | 242 | assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json"); |
| ... | ... | @@ -272,7 +271,6 @@ describe("DesktopConnectionCatalogStore", () => { |
| 272 | 271 | error, |
| 273 | 272 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError, |
| 274 | 273 | ); |
| 275 | assert.equal(error.operation, "read-catalog"); | |
| 276 | 274 | assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`); |
| 277 | 275 | assert.strictEqual(error.cause, permissionError); |
| 278 | 276 | assert.equal( |
| ... | ... | @@ -368,7 +366,6 @@ describe("DesktopConnectionCatalogStore", () => { |
| 368 | 366 | error, |
| 369 | 367 | DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError, |
| 370 | 368 | ); |
| 371 | assert.equal(error.operation, "decode-encrypted-catalog"); | |
| 372 | 369 | assert.equal(error.resource, "encryptedCatalog"); |
| 373 | 370 | assert.equal(error.catalogPath, catalogPath); |
| 374 | 371 | assert.exists(error.cause); |
apps/desktop/src/app/DesktopConnectionCatalogStore.ts+2-11| ... | ... | @@ -92,7 +92,6 @@ const writeError = ( |
| 92 | 92 | export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()( |
| 93 | 93 | "DesktopConnectionCatalogStoreDecodeError", |
| 94 | 94 | { |
| 95 | operation: Schema.Literal("decode-encrypted-catalog"), | |
| 96 | 95 | resource: Schema.Literal("encryptedCatalog"), |
| 97 | 96 | catalogPath: Schema.String, |
| 98 | 97 | cause: Schema.Defect(), |
| ... | ... | @@ -106,7 +105,6 @@ export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedError |
| 106 | 105 | export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()( |
| 107 | 106 | "DesktopConnectionCatalogStoreReadError", |
| 108 | 107 | { |
| 109 | operation: Schema.Literal("read-catalog"), | |
| 110 | 108 | catalogPath: Schema.String, |
| 111 | 109 | cause: Schema.Defect(), |
| 112 | 110 | }, |
| ... | ... | @@ -119,7 +117,6 @@ export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorCl |
| 119 | 117 | export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()( |
| 120 | 118 | "DesktopConnectionCatalogStoreDocumentDecodeError", |
| 121 | 119 | { |
| 122 | operation: Schema.Literal("decode-catalog-document"), | |
| 123 | 120 | catalogPath: Schema.String, |
| 124 | 121 | cause: Schema.Defect(), |
| 125 | 122 | }, |
| ... | ... | @@ -167,16 +164,13 @@ export class DesktopConnectionCatalogStore extends Context.Service< |
| 167 | 164 | | DesktopConnectionCatalogStoreDocumentDecodeError |
| 168 | 165 | | DesktopConnectionCatalogStoreDecodeError |
| 169 | 166 | | DesktopConnectionCatalogStoreMigrationError |
| 170 | | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | |
| 171 | | ElectronSafeStorage.ElectronSafeStorageDecryptError | |
| 167 | | ElectronSafeStorage.ElectronSafeStorageError | |
| 172 | 168 | >; |
| 173 | 169 | readonly set: ( |
| 174 | 170 | catalog: string, |
| 175 | 171 | ) => Effect.Effect< |
| 176 | 172 | boolean, |
| 177 | | DesktopConnectionCatalogStoreWriteError | |
| 178 | | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | |
| 179 | | ElectronSafeStorage.ElectronSafeStorageEncryptError | |
| 173 | DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError | |
| 180 | 174 | >; |
| 181 | 175 | readonly clear: Effect.Effect<void>; |
| 182 | 176 | } |
| ... | ... | @@ -190,7 +184,6 @@ function decodeSecretBytes( |
| 190 | 184 | Effect.mapError( |
| 191 | 185 | (cause) => |
| 192 | 186 | new DesktopConnectionCatalogStoreDecodeError({ |
| 193 | operation: "decode-encrypted-catalog", | |
| 194 | 187 | resource: "encryptedCatalog", |
| 195 | 188 | catalogPath, |
| 196 | 189 | cause, |
| ... | ... | @@ -212,7 +205,6 @@ const readDocument = ( |
| 212 | 205 | ? Effect.succeed<string | null>(null) |
| 213 | 206 | : Effect.fail( |
| 214 | 207 | new DesktopConnectionCatalogStoreReadError({ |
| 215 | operation: "read-catalog", | |
| 216 | 208 | catalogPath, |
| 217 | 209 | cause: error, |
| 218 | 210 | }), |
| ... | ... | @@ -226,7 +218,6 @@ const readDocument = ( |
| 226 | 218 | Effect.mapError( |
| 227 | 219 | (cause) => |
| 228 | 220 | new DesktopConnectionCatalogStoreDocumentDecodeError({ |
| 229 | operation: "decode-catalog-document", | |
| 230 | 221 | catalogPath, |
| 231 | 222 | cause, |
| 232 | 223 | }), |
apps/desktop/src/app/DesktopLifecycle.ts+3-3| ... | ... | @@ -8,7 +8,7 @@ import * as Scope from "effect/Scope"; |
| 8 | 8 | import type * as Electron from "electron"; |
| 9 | 9 | |
| 10 | 10 | import * as DesktopEnvironment from "./DesktopEnvironment.ts"; |
| 11 | import * as DesktopObservability from "./DesktopObservability.ts"; | |
| 11 | import { makeComponentLogger } from "./DesktopObservability.ts"; | |
| 12 | 12 | import * as DesktopShutdown from "./DesktopShutdown.ts"; |
| 13 | 13 | import * as ElectronApp from "../electron/ElectronApp.ts"; |
| 14 | 14 | import * as ElectronTheme from "../electron/ElectronTheme.ts"; |
| ... | ... | @@ -37,7 +37,7 @@ export class DesktopLifecycle extends Context.Service< |
| 37 | 37 | >()("@t3tools/desktop/app/DesktopLifecycle") {} |
| 38 | 38 | |
| 39 | 39 | const { logInfo: logLifecycleInfo, logError: logLifecycleError } = |
| 40 | DesktopObservability.makeComponentLogger("desktop-lifecycle"); | |
| 40 | makeComponentLogger("desktop-lifecycle"); | |
| 41 | 41 | |
| 42 | 42 | function addScopedListener<Args extends ReadonlyArray<unknown>>( |
| 43 | 43 | target: unknown, |
| ... | ... | @@ -122,7 +122,7 @@ function quitFromSignal( |
| 122 | 122 | ); |
| 123 | 123 | } |
| 124 | 124 | |
| 125 | const make = DesktopLifecycle.of({ | |
| 125 | export const make = DesktopLifecycle.of({ | |
| 126 | 126 | relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) { |
| 127 | 127 | const electronApp = yield* ElectronApp.ElectronApp; |
| 128 | 128 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
apps/desktop/src/electron/ElectronApp.ts+35-36| ... | ... | @@ -13,41 +13,40 @@ export interface ElectronAppMetadata { |
| 13 | 13 | readonly runningUnderArm64Translation: boolean; |
| 14 | 14 | } |
| 15 | 15 | |
| 16 | export interface ElectronAppShape { | |
| 17 | readonly metadata: Effect.Effect<ElectronAppMetadata>; | |
| 18 | readonly name: Effect.Effect<string>; | |
| 19 | readonly whenReady: Effect.Effect<void>; | |
| 20 | readonly quit: Effect.Effect<void>; | |
| 21 | readonly exit: (code: number) => Effect.Effect<void>; | |
| 22 | readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>; | |
| 23 | readonly setPath: ( | |
| 24 | name: Parameters<Electron.App["setPath"]>[0], | |
| 25 | path: string, | |
| 26 | ) => Effect.Effect<void>; | |
| 27 | readonly setName: (name: string) => Effect.Effect<void>; | |
| 28 | readonly setAboutPanelOptions: ( | |
| 29 | options: Electron.AboutPanelOptionsOptions, | |
| 30 | ) => Effect.Effect<void>; | |
| 31 | readonly setAppUserModelId: (id: string) => Effect.Effect<void>; | |
| 32 | readonly requestSingleInstanceLock: Effect.Effect<boolean>; | |
| 33 | readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>; | |
| 34 | readonly setAsDefaultProtocolClient: ( | |
| 35 | protocol: string, | |
| 36 | path?: string, | |
| 37 | args?: readonly string[], | |
| 38 | ) => Effect.Effect<boolean>; | |
| 39 | readonly setDesktopName: (desktopName: string) => Effect.Effect<void>; | |
| 40 | readonly setDockIcon: (iconPath: string) => Effect.Effect<void>; | |
| 41 | readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>; | |
| 42 | readonly on: <Args extends ReadonlyArray<unknown>>( | |
| 43 | eventName: string, | |
| 44 | listener: (...args: Args) => void, | |
| 45 | ) => Effect.Effect<void, never, Scope.Scope>; | |
| 46 | } | |
| 47 | ||
| 48 | export class ElectronApp extends Context.Service<ElectronApp, ElectronAppShape>()( | |
| 49 | "@t3tools/desktop/electron/ElectronApp", | |
| 50 | ) {} | |
| 16 | export class ElectronApp extends Context.Service< | |
| 17 | ElectronApp, | |
| 18 | { | |
| 19 | readonly metadata: Effect.Effect<ElectronAppMetadata>; | |
| 20 | readonly name: Effect.Effect<string>; | |
| 21 | readonly whenReady: Effect.Effect<void>; | |
| 22 | readonly quit: Effect.Effect<void>; | |
| 23 | readonly exit: (code: number) => Effect.Effect<void>; | |
| 24 | readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>; | |
| 25 | readonly setPath: ( | |
| 26 | name: Parameters<Electron.App["setPath"]>[0], | |
| 27 | path: string, | |
| 28 | ) => Effect.Effect<void>; | |
| 29 | readonly setName: (name: string) => Effect.Effect<void>; | |
| 30 | readonly setAboutPanelOptions: ( | |
| 31 | options: Electron.AboutPanelOptionsOptions, | |
| 32 | ) => Effect.Effect<void>; | |
| 33 | readonly setAppUserModelId: (id: string) => Effect.Effect<void>; | |
| 34 | readonly requestSingleInstanceLock: Effect.Effect<boolean>; | |
| 35 | readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>; | |
| 36 | readonly setAsDefaultProtocolClient: ( | |
| 37 | protocol: string, | |
| 38 | path?: string, | |
| 39 | args?: readonly string[], | |
| 40 | ) => Effect.Effect<boolean>; | |
| 41 | readonly setDesktopName: (desktopName: string) => Effect.Effect<void>; | |
| 42 | readonly setDockIcon: (iconPath: string) => Effect.Effect<void>; | |
| 43 | readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>; | |
| 44 | readonly on: <Args extends ReadonlyArray<unknown>>( | |
| 45 | eventName: string, | |
| 46 | listener: (...args: Args) => void, | |
| 47 | ) => Effect.Effect<void, never, Scope.Scope>; | |
| 48 | } | |
| 49 | >()("@t3tools/desktop/electron/ElectronApp") {} | |
| 51 | 50 | |
| 52 | 51 | const addScopedAppListener = <Args extends ReadonlyArray<unknown>>( |
| 53 | 52 | eventName: string, |
| ... | ... | @@ -63,7 +62,7 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>( |
| 63 | 62 | }), |
| 64 | 63 | ).pipe(Effect.asVoid); |
| 65 | 64 | |
| 66 | const make = ElectronApp.of({ | |
| 65 | export const make = ElectronApp.of({ | |
| 67 | 66 | metadata: Effect.sync(() => ({ |
| 68 | 67 | appVersion: Electron.app.getVersion(), |
| 69 | 68 | appPath: Electron.app.getAppPath(), |
apps/desktop/src/electron/ElectronDialog.ts+14-15| ... | ... | @@ -17,22 +17,21 @@ export interface ElectronDialogConfirmInput { |
| 17 | 17 | readonly message: string; |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | export interface ElectronDialogShape { | |
| 21 | readonly pickFolder: ( | |
| 22 | input: ElectronDialogPickFolderInput, | |
| 23 | ) => Effect.Effect<Option.Option<string>>; | |
| 24 | readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>; | |
| 25 | readonly showMessageBox: ( | |
| 26 | options: Electron.MessageBoxOptions, | |
| 27 | ) => Effect.Effect<Electron.MessageBoxReturnValue>; | |
| 28 | readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>; | |
| 29 | } | |
| 30 | ||
| 31 | export class ElectronDialog extends Context.Service<ElectronDialog, ElectronDialogShape>()( | |
| 32 | "@t3tools/desktop/electron/ElectronDialog", | |
| 33 | ) {} | |
| 20 | export class ElectronDialog extends Context.Service< | |
| 21 | ElectronDialog, | |
| 22 | { | |
| 23 | readonly pickFolder: ( | |
| 24 | input: ElectronDialogPickFolderInput, | |
| 25 | ) => Effect.Effect<Option.Option<string>>; | |
| 26 | readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>; | |
| 27 | readonly showMessageBox: ( | |
| 28 | options: Electron.MessageBoxOptions, | |
| 29 | ) => Effect.Effect<Electron.MessageBoxReturnValue>; | |
| 30 | readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>; | |
| 31 | } | |
| 32 | >()("@t3tools/desktop/electron/ElectronDialog") {} | |
| 34 | 33 | |
| 35 | const make = ElectronDialog.of({ | |
| 34 | export const make = ElectronDialog.of({ | |
| 36 | 35 | pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) { |
| 37 | 36 | const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, { |
| 38 | 37 | onNone: () => ({ |
apps/desktop/src/electron/ElectronMenu.ts+111-113| ... | ... | @@ -1,11 +1,11 @@ |
| 1 | 1 | import type { ContextMenuItem } from "@t3tools/contracts"; |
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 2 | 3 | import * as Context from "effect/Context"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| 4 | 5 | import * as Layer from "effect/Layer"; |
| 5 | 6 | import * as Option from "effect/Option"; |
| 6 | 7 | |
| 7 | 8 | import * as Electron from "electron"; |
| 8 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 9 | 9 | |
| 10 | 10 | export interface ElectronMenuPosition { |
| 11 | 11 | readonly x: number; |
| ... | ... | @@ -23,19 +23,18 @@ export interface ElectronMenuTemplateInput { |
| 23 | 23 | readonly template: readonly Electron.MenuItemConstructorOptions[]; |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | export interface ElectronMenuShape { | |
| 27 | readonly setApplicationMenu: ( | |
| 28 | template: readonly Electron.MenuItemConstructorOptions[], | |
| 29 | ) => Effect.Effect<void>; | |
| 30 | readonly showContextMenu: ( | |
| 31 | input: ElectronMenuContextInput, | |
| 32 | ) => Effect.Effect<Option.Option<string>>; | |
| 33 | readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>; | |
| 34 | } | |
| 35 | ||
| 36 | export class ElectronMenu extends Context.Service<ElectronMenu, ElectronMenuShape>()( | |
| 37 | "@t3tools/desktop/electron/ElectronMenu", | |
| 38 | ) {} | |
| 26 | export class ElectronMenu extends Context.Service< | |
| 27 | ElectronMenu, | |
| 28 | { | |
| 29 | readonly setApplicationMenu: ( | |
| 30 | template: readonly Electron.MenuItemConstructorOptions[], | |
| 31 | ) => Effect.Effect<void>; | |
| 32 | readonly showContextMenu: ( | |
| 33 | input: ElectronMenuContextInput, | |
| 34 | ) => Effect.Effect<Option.Option<string>>; | |
| 35 | readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>; | |
| 36 | } | |
| 37 | >()("@t3tools/desktop/electron/ElectronMenu") {} | |
| 39 | 38 | |
| 40 | 39 | function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] { |
| 41 | 40 | const normalizedItems: ContextMenuItem[] = []; |
| ... | ... | @@ -80,114 +79,113 @@ const normalizePosition = ( |
| 80 | 79 | ({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0, |
| 81 | 80 | ).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) }))); |
| 82 | 81 | |
| 83 | export const layer = Layer.effect( | |
| 84 | ElectronMenu, | |
| 85 | Effect.gen(function* () { | |
| 86 | const platform = yield* HostProcessPlatform; | |
| 87 | let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined; | |
| 82 | export const make = Effect.gen(function* () { | |
| 83 | const platform = yield* HostProcessPlatform; | |
| 84 | let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined; | |
| 88 | 85 | |
| 89 | const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => { | |
| 90 | if (platform !== "darwin") { | |
| 91 | return Option.none(); | |
| 92 | } | |
| 93 | if (destructiveMenuIconCache !== undefined) { | |
| 94 | return destructiveMenuIconCache; | |
| 95 | } | |
| 86 | const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => { | |
| 87 | if (platform !== "darwin") { | |
| 88 | return Option.none(); | |
| 89 | } | |
| 90 | if (destructiveMenuIconCache !== undefined) { | |
| 91 | return destructiveMenuIconCache; | |
| 92 | } | |
| 96 | 93 | |
| 97 | try { | |
| 98 | const icon = Electron.nativeImage.createFromNamedImage("trash").resize({ | |
| 99 | width: 12, | |
| 100 | height: 12, | |
| 101 | }); | |
| 102 | icon.setTemplateImage(true); | |
| 103 | destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon); | |
| 104 | } catch { | |
| 105 | destructiveMenuIconCache = Option.none(); | |
| 106 | } | |
| 94 | try { | |
| 95 | const icon = Electron.nativeImage.createFromNamedImage("trash").resize({ | |
| 96 | width: 12, | |
| 97 | height: 12, | |
| 98 | }); | |
| 99 | icon.setTemplateImage(true); | |
| 100 | destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon); | |
| 101 | } catch { | |
| 102 | destructiveMenuIconCache = Option.none(); | |
| 103 | } | |
| 107 | 104 | |
| 108 | return destructiveMenuIconCache; | |
| 109 | }; | |
| 105 | return destructiveMenuIconCache; | |
| 106 | }; | |
| 110 | 107 | |
| 111 | const buildTemplate = ( | |
| 112 | entries: readonly ContextMenuItem[], | |
| 113 | complete: (selectedItemId: Option.Option<string>) => void, | |
| 114 | ): Electron.MenuItemConstructorOptions[] => { | |
| 115 | const template: Electron.MenuItemConstructorOptions[] = []; | |
| 116 | let hasInsertedDestructiveSeparator = false; | |
| 117 | ||
| 118 | for (const item of entries) { | |
| 119 | if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) { | |
| 120 | template.push({ type: "separator" }); | |
| 121 | hasInsertedDestructiveSeparator = true; | |
| 122 | } | |
| 108 | const buildTemplate = ( | |
| 109 | entries: readonly ContextMenuItem[], | |
| 110 | complete: (selectedItemId: Option.Option<string>) => void, | |
| 111 | ): Electron.MenuItemConstructorOptions[] => { | |
| 112 | const template: Electron.MenuItemConstructorOptions[] = []; | |
| 113 | let hasInsertedDestructiveSeparator = false; | |
| 123 | 114 | |
| 124 | const itemOption: Electron.MenuItemConstructorOptions = { | |
| 125 | label: item.label, | |
| 126 | enabled: !item.disabled, | |
| 127 | }; | |
| 128 | if (item.children && item.children.length > 0) { | |
| 129 | itemOption.submenu = buildTemplate(item.children, complete); | |
| 130 | } else { | |
| 131 | itemOption.click = () => complete(Option.some(item.id)); | |
| 132 | } | |
| 133 | if (item.destructive && (!item.children || item.children.length === 0)) { | |
| 134 | const destructiveIcon = getDestructiveMenuIcon(); | |
| 135 | if (Option.isSome(destructiveIcon)) { | |
| 136 | itemOption.icon = destructiveIcon.value; | |
| 137 | } | |
| 138 | } | |
| 115 | for (const item of entries) { | |
| 116 | if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) { | |
| 117 | template.push({ type: "separator" }); | |
| 118 | hasInsertedDestructiveSeparator = true; | |
| 119 | } | |
| 139 | 120 | |
| 140 | template.push(itemOption); | |
| 121 | const itemOption: Electron.MenuItemConstructorOptions = { | |
| 122 | label: item.label, | |
| 123 | enabled: !item.disabled, | |
| 124 | }; | |
| 125 | if (item.children && item.children.length > 0) { | |
| 126 | itemOption.submenu = buildTemplate(item.children, complete); | |
| 127 | } else { | |
| 128 | itemOption.click = () => complete(Option.some(item.id)); | |
| 129 | } | |
| 130 | if (item.destructive && (!item.children || item.children.length === 0)) { | |
| 131 | const destructiveIcon = getDestructiveMenuIcon(); | |
| 132 | if (Option.isSome(destructiveIcon)) { | |
| 133 | itemOption.icon = destructiveIcon.value; | |
| 134 | } | |
| 141 | 135 | } |
| 142 | 136 | |
| 143 | return template; | |
| 144 | }; | |
| 137 | template.push(itemOption); | |
| 138 | } | |
| 145 | 139 | |
| 146 | return ElectronMenu.of({ | |
| 147 | setApplicationMenu: (template) => | |
| 148 | Effect.sync(() => { | |
| 149 | Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template])); | |
| 150 | }), | |
| 151 | popupTemplate: (input) => | |
| 152 | Effect.sync(() => { | |
| 153 | if (input.template.length === 0) { | |
| 154 | return; | |
| 155 | } | |
| 156 | Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window }); | |
| 157 | }), | |
| 158 | showContextMenu: (input) => | |
| 159 | Effect.callback<Option.Option<string>>((resume) => { | |
| 160 | const normalizedItems = normalizeContextMenuItems(input.items); | |
| 161 | if (normalizedItems.length === 0) { | |
| 162 | resume(Effect.succeed(Option.none())); | |
| 140 | return template; | |
| 141 | }; | |
| 142 | ||
| 143 | return ElectronMenu.of({ | |
| 144 | setApplicationMenu: (template) => | |
| 145 | Effect.sync(() => { | |
| 146 | Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template])); | |
| 147 | }), | |
| 148 | popupTemplate: (input) => | |
| 149 | Effect.sync(() => { | |
| 150 | if (input.template.length === 0) { | |
| 151 | return; | |
| 152 | } | |
| 153 | Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window }); | |
| 154 | }), | |
| 155 | showContextMenu: (input) => | |
| 156 | Effect.callback<Option.Option<string>>((resume) => { | |
| 157 | const normalizedItems = normalizeContextMenuItems(input.items); | |
| 158 | if (normalizedItems.length === 0) { | |
| 159 | resume(Effect.succeed(Option.none())); | |
| 160 | return; | |
| 161 | } | |
| 162 | ||
| 163 | let completed = false; | |
| 164 | const complete = (selectedItemId: Option.Option<string>) => { | |
| 165 | if (completed) { | |
| 163 | 166 | return; |
| 164 | 167 | } |
| 168 | completed = true; | |
| 169 | resume(Effect.succeed(selectedItemId)); | |
| 170 | }; | |
| 171 | ||
| 172 | const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete)); | |
| 173 | const popupPosition = normalizePosition(input.position); | |
| 174 | const popupOptions = Option.match(popupPosition, { | |
| 175 | onNone: (): Electron.PopupOptions => ({ | |
| 176 | window: input.window, | |
| 177 | callback: () => complete(Option.none()), | |
| 178 | }), | |
| 179 | onSome: (position): Electron.PopupOptions => ({ | |
| 180 | window: input.window, | |
| 181 | x: position.x, | |
| 182 | y: position.y, | |
| 183 | callback: () => complete(Option.none()), | |
| 184 | }), | |
| 185 | }); | |
| 186 | menu.popup(popupOptions); | |
| 187 | }), | |
| 188 | }); | |
| 189 | }); | |
| 165 | 190 | |
| 166 | let completed = false; | |
| 167 | const complete = (selectedItemId: Option.Option<string>) => { | |
| 168 | if (completed) { | |
| 169 | return; | |
| 170 | } | |
| 171 | completed = true; | |
| 172 | resume(Effect.succeed(selectedItemId)); | |
| 173 | }; | |
| 174 | ||
| 175 | const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete)); | |
| 176 | const popupPosition = normalizePosition(input.position); | |
| 177 | const popupOptions = Option.match(popupPosition, { | |
| 178 | onNone: (): Electron.PopupOptions => ({ | |
| 179 | window: input.window, | |
| 180 | callback: () => complete(Option.none()), | |
| 181 | }), | |
| 182 | onSome: (position): Electron.PopupOptions => ({ | |
| 183 | window: input.window, | |
| 184 | x: position.x, | |
| 185 | y: position.y, | |
| 186 | callback: () => complete(Option.none()), | |
| 187 | }), | |
| 188 | }); | |
| 189 | menu.popup(popupOptions); | |
| 190 | }), | |
| 191 | }); | |
| 192 | }), | |
| 193 | ); | |
| 191 | export const layer = Layer.effect(ElectronMenu, make); |
apps/desktop/src/electron/ElectronProtocol.ts+17-17| ... | ... | @@ -1,8 +1,8 @@ |
| 1 | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | |
| 3 | 2 | import * as Effect from "effect/Effect"; |
| 4 | 3 | import * as Layer from "effect/Layer"; |
| 5 | 4 | import * as Ref from "effect/Ref"; |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | 6 | import * as Scope from "effect/Scope"; |
| 7 | 7 | |
| 8 | 8 | import * as Electron from "electron"; |
| ... | ... | @@ -23,13 +23,14 @@ export function getDesktopUrl(isDevelopment: boolean): string { |
| 23 | 23 | return `${getDesktopOrigin(isDevelopment)}/`; |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | export class ElectronProtocolRegistrationError extends Data.TaggedError( | |
| 26 | export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass<ElectronProtocolRegistrationError>()( | |
| 27 | 27 | "ElectronProtocolRegistrationError", |
| 28 | )<{ | |
| 29 | readonly scheme: string; | |
| 30 | readonly cause: unknown; | |
| 31 | }> { | |
| 32 | override get message() { | |
| 28 | { | |
| 29 | scheme: Schema.String, | |
| 30 | cause: Schema.Defect(), | |
| 31 | }, | |
| 32 | ) { | |
| 33 | override get message(): string { | |
| 33 | 34 | return `Failed to register ${this.scheme}: protocol.`; |
| 34 | 35 | } |
| 35 | 36 | } |
| ... | ... | @@ -41,15 +42,14 @@ export interface DesktopProtocolRegistrationInput { |
| 41 | 42 | readonly clerkFrontendApiHostname: string | undefined; |
| 42 | 43 | } |
| 43 | 44 | |
| 44 | export interface ElectronProtocolShape { | |
| 45 | readonly registerDesktopProtocol: ( | |
| 46 | input: DesktopProtocolRegistrationInput, | |
| 47 | ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>; | |
| 48 | } | |
| 49 | ||
| 50 | export class ElectronProtocol extends Context.Service<ElectronProtocol, ElectronProtocolShape>()( | |
| 51 | "@t3tools/desktop/electron/ElectronProtocol", | |
| 52 | ) {} | |
| 45 | export class ElectronProtocol extends Context.Service< | |
| 46 | ElectronProtocol, | |
| 47 | { | |
| 48 | readonly registerDesktopProtocol: ( | |
| 49 | input: DesktopProtocolRegistrationInput, | |
| 50 | ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>; | |
| 51 | } | |
| 52 | >()("@t3tools/desktop/electron/ElectronProtocol") {} | |
| 53 | 53 | |
| 54 | 54 | export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string { |
| 55 | 55 | const clerkOrigin = input.clerkFrontendApiHostname |
| ... | ... | @@ -114,7 +114,7 @@ async function proxyRequest( |
| 114 | 114 | return withContentSecurityPolicy(response, contentSecurityPolicy); |
| 115 | 115 | } |
| 116 | 116 | |
| 117 | const make = Effect.gen(function* () { | |
| 117 | export const make = Effect.gen(function* () { | |
| 118 | 118 | const registered = yield* Ref.make(false); |
| 119 | 119 | |
| 120 | 120 | const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")( |
apps/desktop/src/electron/ElectronSafeStorage.ts+43-30| ... | ... | @@ -1,56 +1,69 @@ |
| 1 | import * as Electron from "electron"; | |
| 2 | ||
| 1 | import * as Context from "effect/Context"; | |
| 3 | 2 | import * as Effect from "effect/Effect"; |
| 4 | 3 | import * as Layer from "effect/Layer"; |
| 5 | import * as Context from "effect/Context"; | |
| 6 | import * as Data from "effect/Data"; | |
| 4 | import * as Schema from "effect/Schema"; | |
| 7 | 5 | |
| 8 | export class ElectronSafeStorageAvailabilityError extends Data.TaggedError( | |
| 6 | import * as Electron from "electron"; | |
| 7 | ||
| 8 | const electronSafeStorageErrorFields = { | |
| 9 | cause: Schema.Defect(), | |
| 10 | }; | |
| 11 | ||
| 12 | export class ElectronSafeStorageAvailabilityError extends Schema.TaggedErrorClass<ElectronSafeStorageAvailabilityError>()( | |
| 9 | 13 | "ElectronSafeStorageAvailabilityError", |
| 10 | )<{ | |
| 11 | readonly cause: unknown; | |
| 12 | }> { | |
| 13 | override get message() { | |
| 14 | { | |
| 15 | ...electronSafeStorageErrorFields, | |
| 16 | }, | |
| 17 | ) { | |
| 18 | override get message(): string { | |
| 14 | 19 | return "Electron safe storage failed to check encryption availability."; |
| 15 | 20 | } |
| 16 | 21 | } |
| 17 | 22 | |
| 18 | export class ElectronSafeStorageEncryptError extends Data.TaggedError( | |
| 23 | export class ElectronSafeStorageEncryptError extends Schema.TaggedErrorClass<ElectronSafeStorageEncryptError>()( | |
| 19 | 24 | "ElectronSafeStorageEncryptError", |
| 20 | )<{ | |
| 21 | readonly cause: unknown; | |
| 22 | }> { | |
| 23 | override get message() { | |
| 25 | { | |
| 26 | ...electronSafeStorageErrorFields, | |
| 27 | }, | |
| 28 | ) { | |
| 29 | override get message(): string { | |
| 24 | 30 | return "Electron safe storage failed to encrypt a string."; |
| 25 | 31 | } |
| 26 | 32 | } |
| 27 | 33 | |
| 28 | export class ElectronSafeStorageDecryptError extends Data.TaggedError( | |
| 34 | export class ElectronSafeStorageDecryptError extends Schema.TaggedErrorClass<ElectronSafeStorageDecryptError>()( | |
| 29 | 35 | "ElectronSafeStorageDecryptError", |
| 30 | )<{ | |
| 31 | readonly cause: unknown; | |
| 32 | }> { | |
| 33 | override get message() { | |
| 36 | { | |
| 37 | ...electronSafeStorageErrorFields, | |
| 38 | }, | |
| 39 | ) { | |
| 40 | override get message(): string { | |
| 34 | 41 | return "Electron safe storage failed to decrypt a string."; |
| 35 | 42 | } |
| 36 | 43 | } |
| 37 | 44 | |
| 38 | export interface ElectronSafeStorageShape { | |
| 39 | readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>; | |
| 40 | readonly encryptString: ( | |
| 41 | value: string, | |
| 42 | ) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>; | |
| 43 | readonly decryptString: ( | |
| 44 | value: Uint8Array, | |
| 45 | ) => Effect.Effect<string, ElectronSafeStorageDecryptError>; | |
| 46 | } | |
| 45 | export const ElectronSafeStorageError = Schema.Union([ | |
| 46 | ElectronSafeStorageAvailabilityError, | |
| 47 | ElectronSafeStorageEncryptError, | |
| 48 | ElectronSafeStorageDecryptError, | |
| 49 | ]); | |
| 50 | export type ElectronSafeStorageError = typeof ElectronSafeStorageError.Type; | |
| 51 | export const isElectronSafeStorageError = Schema.is(ElectronSafeStorageError); | |
| 47 | 52 | |
| 48 | 53 | export class ElectronSafeStorage extends Context.Service< |
| 49 | 54 | ElectronSafeStorage, |
| 50 | ElectronSafeStorageShape | |
| 55 | { | |
| 56 | readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>; | |
| 57 | readonly encryptString: ( | |
| 58 | value: string, | |
| 59 | ) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>; | |
| 60 | readonly decryptString: ( | |
| 61 | value: Uint8Array, | |
| 62 | ) => Effect.Effect<string, ElectronSafeStorageDecryptError>; | |
| 63 | } | |
| 51 | 64 | >()("@t3tools/desktop/electron/ElectronSafeStorage") {} |
| 52 | 65 | |
| 53 | const make = ElectronSafeStorage.of({ | |
| 66 | export const make = ElectronSafeStorage.of({ | |
| 54 | 67 | isEncryptionAvailable: Effect.try({ |
| 55 | 68 | try: () => Electron.safeStorage.isEncryptionAvailable(), |
| 56 | 69 | catch: (cause) => new ElectronSafeStorageAvailabilityError({ cause }), |
apps/desktop/src/electron/ElectronShell.ts+8-9| ... | ... | @@ -20,16 +20,15 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> { |
| 20 | 20 | } |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | export interface ElectronShellShape { | |
| 24 | readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>; | |
| 25 | readonly copyText: (text: string) => Effect.Effect<void>; | |
| 26 | } | |
| 27 | ||
| 28 | export class ElectronShell extends Context.Service<ElectronShell, ElectronShellShape>()( | |
| 29 | "@t3tools/desktop/electron/ElectronShell", | |
| 30 | ) {} | |
| 23 | export class ElectronShell extends Context.Service< | |
| 24 | ElectronShell, | |
| 25 | { | |
| 26 | readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>; | |
| 27 | readonly copyText: (text: string) => Effect.Effect<void>; | |
| 28 | } | |
| 29 | >()("@t3tools/desktop/electron/ElectronShell") {} | |
| 31 | 30 | |
| 32 | const make = ElectronShell.of({ | |
| 31 | export const make = ElectronShell.of({ | |
| 33 | 32 | openExternal: (rawUrl) => |
| 34 | 33 | Option.match(parseSafeExternalUrl(rawUrl), { |
| 35 | 34 | onNone: () => Effect.succeed(false), |
apps/desktop/src/electron/ElectronTheme.ts+9-10| ... | ... | @@ -6,17 +6,16 @@ import * as Scope from "effect/Scope"; |
| 6 | 6 | |
| 7 | 7 | import * as Electron from "electron"; |
| 8 | 8 | |
| 9 | export interface ElectronThemeShape { | |
| 10 | readonly shouldUseDarkColors: Effect.Effect<boolean>; | |
| 11 | readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>; | |
| 12 | readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>; | |
| 13 | } | |
| 9 | export class ElectronTheme extends Context.Service< | |
| 10 | ElectronTheme, | |
| 11 | { | |
| 12 | readonly shouldUseDarkColors: Effect.Effect<boolean>; | |
| 13 | readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>; | |
| 14 | readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>; | |
| 15 | } | |
| 16 | >()("@t3tools/desktop/electron/ElectronTheme") {} | |
| 14 | 17 | |
| 15 | export class ElectronTheme extends Context.Service<ElectronTheme, ElectronThemeShape>()( | |
| 16 | "@t3tools/desktop/electron/ElectronTheme", | |
| 17 | ) {} | |
| 18 | ||
| 19 | const make = ElectronTheme.of({ | |
| 18 | export const make = ElectronTheme.of({ | |
| 20 | 19 | shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors), |
| 21 | 20 | setSource: (theme) => |
| 22 | 21 | Effect.suspend(() => { |
apps/desktop/src/electron/ElectronUpdater.test.ts+1| ... | ... | @@ -73,6 +73,7 @@ describe("ElectronUpdater", () => { |
| 73 | 73 | const error = Cause.squash(exit.cause); |
| 74 | 74 | assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError); |
| 75 | 75 | assert.equal(error.cause, cause); |
| 76 | assert.equal(error.message, "Electron updater failed to check for updates."); | |
| 76 | 77 | } |
| 77 | 78 | }).pipe(Effect.provide(ElectronUpdater.layer)), |
| 78 | 79 | ); |
apps/desktop/src/electron/ElectronUpdater.ts+57-46| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | |
| 3 | 2 | import * as Effect from "effect/Effect"; |
| 4 | 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Schema from "effect/Schema"; | |
| 5 | 5 | import * as Scope from "effect/Scope"; |
| 6 | 6 | |
| 7 | 7 | import { autoUpdater } from "electron-updater"; |
| ... | ... | @@ -10,67 +10,76 @@ type AutoUpdater = typeof autoUpdater; |
| 10 | 10 | |
| 11 | 11 | export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0]; |
| 12 | 12 | |
| 13 | export class ElectronUpdaterCheckForUpdatesError extends Data.TaggedError( | |
| 13 | const electronUpdaterErrorFields = { | |
| 14 | cause: Schema.Defect(), | |
| 15 | }; | |
| 16 | ||
| 17 | export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass<ElectronUpdaterCheckForUpdatesError>()( | |
| 14 | 18 | "ElectronUpdaterCheckForUpdatesError", |
| 15 | )<{ | |
| 16 | readonly cause: unknown; | |
| 17 | }> { | |
| 18 | override get message() { | |
| 19 | { | |
| 20 | ...electronUpdaterErrorFields, | |
| 21 | }, | |
| 22 | ) { | |
| 23 | override get message(): string { | |
| 19 | 24 | return "Electron updater failed to check for updates."; |
| 20 | 25 | } |
| 21 | 26 | } |
| 22 | 27 | |
| 23 | export class ElectronUpdaterDownloadUpdateError extends Data.TaggedError( | |
| 28 | export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()( | |
| 24 | 29 | "ElectronUpdaterDownloadUpdateError", |
| 25 | )<{ | |
| 26 | readonly cause: unknown; | |
| 27 | }> { | |
| 28 | override get message() { | |
| 30 | { | |
| 31 | ...electronUpdaterErrorFields, | |
| 32 | }, | |
| 33 | ) { | |
| 34 | override get message(): string { | |
| 29 | 35 | return "Electron updater failed to download the update."; |
| 30 | 36 | } |
| 31 | 37 | } |
| 32 | 38 | |
| 33 | export class ElectronUpdaterQuitAndInstallError extends Data.TaggedError( | |
| 39 | export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()( | |
| 34 | 40 | "ElectronUpdaterQuitAndInstallError", |
| 35 | )<{ | |
| 36 | readonly cause: unknown; | |
| 37 | }> { | |
| 38 | override get message() { | |
| 41 | { | |
| 42 | ...electronUpdaterErrorFields, | |
| 43 | }, | |
| 44 | ) { | |
| 45 | override get message(): string { | |
| 39 | 46 | return "Electron updater failed to quit and install the update."; |
| 40 | 47 | } |
| 41 | 48 | } |
| 42 | 49 | |
| 43 | export type ElectronUpdaterError = | |
| 44 | | ElectronUpdaterCheckForUpdatesError | |
| 45 | | ElectronUpdaterDownloadUpdateError | |
| 46 | | ElectronUpdaterQuitAndInstallError; | |
| 50 | export const ElectronUpdaterError = Schema.Union([ | |
| 51 | ElectronUpdaterCheckForUpdatesError, | |
| 52 | ElectronUpdaterDownloadUpdateError, | |
| 53 | ElectronUpdaterQuitAndInstallError, | |
| 54 | ]); | |
| 55 | export type ElectronUpdaterError = typeof ElectronUpdaterError.Type; | |
| 56 | export const isElectronUpdaterError = Schema.is(ElectronUpdaterError); | |
| 47 | 57 | |
| 48 | export interface ElectronUpdaterShape { | |
| 49 | readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>; | |
| 50 | readonly setAutoDownload: (value: boolean) => Effect.Effect<void>; | |
| 51 | readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>; | |
| 52 | readonly setChannel: (channel: string) => Effect.Effect<void>; | |
| 53 | readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>; | |
| 54 | readonly allowDowngrade: Effect.Effect<boolean>; | |
| 55 | readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>; | |
| 56 | readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>; | |
| 57 | readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>; | |
| 58 | readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>; | |
| 59 | readonly quitAndInstall: (options: { | |
| 60 | readonly isSilent: boolean; | |
| 61 | readonly isForceRunAfter: boolean; | |
| 62 | }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>; | |
| 63 | readonly on: <Args extends ReadonlyArray<unknown>>( | |
| 64 | eventName: string, | |
| 65 | listener: (...args: Args) => void, | |
| 66 | ) => Effect.Effect<void, never, Scope.Scope>; | |
| 67 | } | |
| 68 | ||
| 69 | export class ElectronUpdater extends Context.Service<ElectronUpdater, ElectronUpdaterShape>()( | |
| 70 | "@t3tools/desktop/electron/ElectronUpdater", | |
| 71 | ) {} | |
| 58 | export class ElectronUpdater extends Context.Service< | |
| 59 | ElectronUpdater, | |
| 60 | { | |
| 61 | readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>; | |
| 62 | readonly setAutoDownload: (value: boolean) => Effect.Effect<void>; | |
| 63 | readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>; | |
| 64 | readonly setChannel: (channel: string) => Effect.Effect<void>; | |
| 65 | readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>; | |
| 66 | readonly allowDowngrade: Effect.Effect<boolean>; | |
| 67 | readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>; | |
| 68 | readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>; | |
| 69 | readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>; | |
| 70 | readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>; | |
| 71 | readonly quitAndInstall: (options: { | |
| 72 | readonly isSilent: boolean; | |
| 73 | readonly isForceRunAfter: boolean; | |
| 74 | }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>; | |
| 75 | readonly on: <Args extends ReadonlyArray<unknown>>( | |
| 76 | eventName: string, | |
| 77 | listener: (...args: Args) => void, | |
| 78 | ) => Effect.Effect<void, never, Scope.Scope>; | |
| 79 | } | |
| 80 | >()("@t3tools/desktop/electron/ElectronUpdater") {} | |
| 72 | 81 | |
| 73 | export const layer = Layer.succeed(ElectronUpdater, { | |
| 82 | export const make = ElectronUpdater.of({ | |
| 74 | 83 | setFeedURL: (options) => |
| 75 | 84 | Effect.suspend(() => { |
| 76 | 85 | autoUpdater.setFeedURL(options); |
| ... | ... | @@ -136,4 +145,6 @@ export const layer = Layer.succeed(ElectronUpdater, { |
| 136 | 145 | }), |
| 137 | 146 | ).pipe(Effect.asVoid); |
| 138 | 147 | }, |
| 139 | } satisfies ElectronUpdaterShape); | |
| 148 | }); | |
| 149 | ||
| 150 | export const layer = Layer.succeed(ElectronUpdater, make); |
apps/desktop/src/electron/ElectronWindow.ts+29-27| ... | ... | @@ -1,43 +1,45 @@ |
| 1 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 1 | 2 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as Layer from "effect/Layer"; |
| 5 | 5 | import * as Option from "effect/Option"; |
| 6 | 6 | import * as Ref from "effect/Ref"; |
| 7 | import * as Schema from "effect/Schema"; | |
| 7 | 8 | |
| 8 | 9 | import * as Electron from "electron"; |
| 9 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 10 | 10 | |
| 11 | export class ElectronWindowCreateError extends Data.TaggedError("ElectronWindowCreateError")<{ | |
| 12 | readonly cause: unknown; | |
| 13 | }> { | |
| 14 | override get message() { | |
| 11 | export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()( | |
| 12 | "ElectronWindowCreateError", | |
| 13 | { | |
| 14 | cause: Schema.Defect(), | |
| 15 | }, | |
| 16 | ) { | |
| 17 | override get message(): string { | |
| 15 | 18 | return "Failed to create Electron BrowserWindow."; |
| 16 | 19 | } |
| 17 | 20 | } |
| 18 | 21 | |
| 19 | export interface ElectronWindowShape { | |
| 20 | readonly create: ( | |
| 21 | options: Electron.BrowserWindowConstructorOptions, | |
| 22 | ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>; | |
| 23 | readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 24 | readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 25 | readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 26 | readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>; | |
| 27 | readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>; | |
| 28 | readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>; | |
| 29 | readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>; | |
| 30 | readonly destroyAll: Effect.Effect<void>; | |
| 31 | readonly syncAllAppearance: <E, R>( | |
| 32 | sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, | |
| 33 | ) => Effect.Effect<void, E, R>; | |
| 34 | } | |
| 35 | ||
| 36 | export class ElectronWindow extends Context.Service<ElectronWindow, ElectronWindowShape>()( | |
| 37 | "@t3tools/desktop/electron/ElectronWindow", | |
| 38 | ) {} | |
| 22 | export class ElectronWindow extends Context.Service< | |
| 23 | ElectronWindow, | |
| 24 | { | |
| 25 | readonly create: ( | |
| 26 | options: Electron.BrowserWindowConstructorOptions, | |
| 27 | ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>; | |
| 28 | readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 29 | readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 30 | readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>; | |
| 31 | readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>; | |
| 32 | readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>; | |
| 33 | readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>; | |
| 34 | readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>; | |
| 35 | readonly destroyAll: Effect.Effect<void>; | |
| 36 | readonly syncAllAppearance: <E, R>( | |
| 37 | sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>, | |
| 38 | ) => Effect.Effect<void, E, R>; | |
| 39 | } | |
| 40 | >()("@t3tools/desktop/electron/ElectronWindow") {} | |
| 39 | 41 | |
| 40 | const make = Effect.gen(function* () { | |
| 42 | export const make = Effect.gen(function* () { | |
| 41 | 43 | const platform = yield* HostProcessPlatform; |
| 42 | 44 | const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none()); |
| 43 | 45 |
apps/desktop/src/main.ts+4-4| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"; |
| 2 | 2 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 3 | 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import * as NodeOS from "node:os"; | |
| 4 | import { homedir } from "node:os"; | |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | 6 | import * as Layer from "effect/Layer"; |
| 7 | 7 | import * as Option from "effect/Option"; |
| ... | ... | @@ -19,7 +19,7 @@ import * as ElectronApp from "./electron/ElectronApp.ts"; |
| 19 | 19 | import * as ElectronDialog from "./electron/ElectronDialog.ts"; |
| 20 | 20 | import * as ElectronMenu from "./electron/ElectronMenu.ts"; |
| 21 | 21 | import * as ElectronProtocol from "./electron/ElectronProtocol.ts"; |
| 22 | import * as DesktopSecretStorage from "./electron/ElectronSafeStorage.ts"; | |
| 22 | import * as ElectronSafeStorage from "./electron/ElectronSafeStorage.ts"; | |
| 23 | 23 | import * as ElectronShell from "./electron/ElectronShell.ts"; |
| 24 | 24 | import * as ElectronTheme from "./electron/ElectronTheme.ts"; |
| 25 | 25 | import * as ElectronUpdater from "./electron/ElectronUpdater.ts"; |
| ... | ... | @@ -60,7 +60,7 @@ const desktopEnvironmentLayer = Layer.unwrap( |
| 60 | 60 | const processArch = yield* HostProcessArchitecture; |
| 61 | 61 | return DesktopEnvironment.layer({ |
| 62 | 62 | dirname: __dirname, |
| 63 | homeDirectory: NodeOS.homedir(), | |
| 63 | homeDirectory: homedir(), | |
| 64 | 64 | platform, |
| 65 | 65 | processArch, |
| 66 | 66 | ...metadata, |
| ... | ... | @@ -106,7 +106,7 @@ const electronLayer = Layer.mergeAll( |
| 106 | 106 | ElectronDialog.layer, |
| 107 | 107 | ElectronMenu.layer, |
| 108 | 108 | ElectronProtocol.layer, |
| 109 | DesktopSecretStorage.layer, | |
| 109 | ElectronSafeStorage.layer, | |
| 110 | 110 | ElectronShell.layer, |
| 111 | 111 | ElectronTheme.layer, |
| 112 | 112 | ElectronUpdater.layer, |
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts-3| ... | ... | @@ -241,7 +241,6 @@ describe("DesktopSavedEnvironments", () => { |
| 241 | 241 | .getSecret(savedRegistryRecord.environmentId) |
| 242 | 242 | .pipe(Effect.flip); |
| 243 | 243 | assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentSecretDecodeError); |
| 244 | assert.equal(error.operation, "decode-secret"); | |
| 245 | 244 | assert.equal(error.environmentId, savedRegistryRecord.environmentId); |
| 246 | 245 | assert.equal(error.registryPath, environment.savedEnvironmentRegistryPath); |
| 247 | 246 | assert.equal(error.field, "encryptedBearerToken"); |
| ... | ... | @@ -363,7 +362,6 @@ describe("DesktopSavedEnvironments", () => { |
| 363 | 362 | registryError, |
| 364 | 363 | DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError, |
| 365 | 364 | ); |
| 366 | assert.equal(registryError.operation, "decode-registry"); | |
| 367 | 365 | assert.equal(registryError.registryPath, environment.savedEnvironmentRegistryPath); |
| 368 | 366 | assert.exists(registryError.cause); |
| 369 | 367 | const secretError = yield* savedEnvironments |
| ... | ... | @@ -409,7 +407,6 @@ describe("DesktopSavedEnvironments", () => { |
| 409 | 407 | |
| 410 | 408 | const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip); |
| 411 | 409 | assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsReadError); |
| 412 | assert.equal(error.operation, "read-registry"); | |
| 413 | 410 | assert.equal(error.registryPath, registryPath); |
| 414 | 411 | assert.strictEqual(error.cause, permissionError); |
| 415 | 412 | assert.equal(error.message, `Failed to read desktop saved environments at ${registryPath}.`); |
apps/desktop/src/settings/DesktopSavedEnvironments.ts+2-10| ... | ... | @@ -106,7 +106,6 @@ const writeError = ( |
| 106 | 106 | export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsReadError>()( |
| 107 | 107 | "DesktopSavedEnvironmentsReadError", |
| 108 | 108 | { |
| 109 | operation: Schema.Literal("read-registry"), | |
| 110 | 109 | registryPath: Schema.String, |
| 111 | 110 | cause: Schema.Defect(), |
| 112 | 111 | }, |
| ... | ... | @@ -119,7 +118,6 @@ export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<D |
| 119 | 118 | export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsDocumentDecodeError>()( |
| 120 | 119 | "DesktopSavedEnvironmentsDocumentDecodeError", |
| 121 | 120 | { |
| 122 | operation: Schema.Literal("decode-registry"), | |
| 123 | 121 | registryPath: Schema.String, |
| 124 | 122 | cause: Schema.Defect(), |
| 125 | 123 | }, |
| ... | ... | @@ -132,7 +130,6 @@ export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedEr |
| 132 | 130 | export class DesktopSavedEnvironmentSecretDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentSecretDecodeError>()( |
| 133 | 131 | "DesktopSavedEnvironmentSecretDecodeError", |
| 134 | 132 | { |
| 135 | operation: Schema.Literal("decode-secret"), | |
| 136 | 133 | environmentId: Schema.String, |
| 137 | 134 | registryPath: Schema.String, |
| 138 | 135 | field: Schema.Literal("encryptedBearerToken"), |
| ... | ... | @@ -155,13 +152,11 @@ export type DesktopSavedEnvironmentsMutationError = |
| 155 | 152 | export type DesktopSavedEnvironmentsGetSecretError = |
| 156 | 153 | | DesktopSavedEnvironmentsReadRegistryError |
| 157 | 154 | | DesktopSavedEnvironmentSecretDecodeError |
| 158 | | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | |
| 159 | | ElectronSafeStorage.ElectronSafeStorageDecryptError; | |
| 155 | | ElectronSafeStorage.ElectronSafeStorageError; | |
| 160 | 156 | |
| 161 | 157 | export type DesktopSavedEnvironmentsSetSecretError = |
| 162 | 158 | | DesktopSavedEnvironmentsMutationError |
| 163 | | ElectronSafeStorage.ElectronSafeStorageAvailabilityError | |
| 164 | | ElectronSafeStorage.ElectronSafeStorageEncryptError; | |
| 159 | | ElectronSafeStorage.ElectronSafeStorageError; | |
| 165 | 160 | |
| 166 | 161 | export class DesktopSavedEnvironments extends Context.Service< |
| 167 | 162 | DesktopSavedEnvironments, |
| ... | ... | @@ -248,7 +243,6 @@ function readRegistryDocument( |
| 248 | 243 | ? Effect.succeed<string | null>(null) |
| 249 | 244 | : Effect.fail( |
| 250 | 245 | new DesktopSavedEnvironmentsReadError({ |
| 251 | operation: "read-registry", | |
| 252 | 246 | registryPath, |
| 253 | 247 | cause: error, |
| 254 | 248 | }), |
| ... | ... | @@ -262,7 +256,6 @@ function readRegistryDocument( |
| 262 | 256 | Effect.mapError( |
| 263 | 257 | (cause) => |
| 264 | 258 | new DesktopSavedEnvironmentsDocumentDecodeError({ |
| 265 | operation: "decode-registry", | |
| 266 | 259 | registryPath, |
| 267 | 260 | cause, |
| 268 | 261 | }), |
| ... | ... | @@ -329,7 +322,6 @@ function decodeSecretBytes( |
| 329 | 322 | Effect.mapError( |
| 330 | 323 | (cause) => |
| 331 | 324 | new DesktopSavedEnvironmentSecretDecodeError({ |
| 332 | operation: "decode-secret", | |
| 333 | 325 | environmentId, |
| 334 | 326 | registryPath, |
| 335 | 327 | field: "encryptedBearerToken", |
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+2-2| ... | ... | @@ -9,7 +9,7 @@ import * as TestClock from "effect/testing/TestClock"; |
| 9 | 9 | import type * as Electron from "electron"; |
| 10 | 10 | |
| 11 | 11 | import * as ElectronWindow from "../electron/ElectronWindow.ts"; |
| 12 | import * as IpcChannels from "../ipc/channels.ts"; | |
| 12 | import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts"; | |
| 13 | 13 | import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts"; |
| 14 | 14 | |
| 15 | 15 | interface SentMessage { |
| ... | ... | @@ -111,7 +111,7 @@ describe("DesktopSshPasswordPrompts", () => { |
| 111 | 111 | assert.equal(testWindow.sentMessages.length, 1); |
| 112 | 112 | const sent = testWindow.sentMessages[0]; |
| 113 | 113 | assert.ok(sent); |
| 114 | assert.equal(sent.channel, IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL); | |
| 114 | assert.equal(sent.channel, SSH_PASSWORD_PROMPT_CHANNEL); | |
| 115 | 115 | const request = sent.args[0] as { readonly requestId: string; readonly destination: string }; |
| 116 | 116 | assert.equal(request.destination, "devbox"); |
| 117 | 117 | assert.equal(testWindow.isRestored(), true); |
apps/desktop/src/updates/DesktopUpdates.test.ts+2-2| ... | ... | @@ -83,7 +83,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { |
| 83 | 83 | removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void); |
| 84 | 84 | }), |
| 85 | 85 | ).pipe(Effect.asVoid), |
| 86 | } satisfies ElectronUpdater.ElectronUpdaterShape); | |
| 86 | } satisfies ElectronUpdater.ElectronUpdater["Service"]); | |
| 87 | 87 | |
| 88 | 88 | const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, { |
| 89 | 89 | create: () => Effect.die("unexpected BrowserWindow creation"), |
| ... | ... | @@ -99,7 +99,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) { |
| 99 | 99 | }), |
| 100 | 100 | destroyAll: Effect.void, |
| 101 | 101 | syncAllAppearance: () => Effect.void, |
| 102 | } satisfies ElectronWindow.ElectronWindowShape); | |
| 102 | } satisfies ElectronWindow.ElectronWindow["Service"]); | |
| 103 | 103 | |
| 104 | 104 | const backendLayer = Layer.succeed(DesktopBackendManager.DesktopBackendManager, { |
| 105 | 105 | start: Effect.void, |
apps/desktop/src/window/DesktopApplicationMenu.test.ts+3-3| ... | ... | @@ -46,14 +46,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, { |
| 46 | 46 | setDockIcon: () => Effect.void, |
| 47 | 47 | appendCommandLineSwitch: () => Effect.void, |
| 48 | 48 | on: () => Effect.void, |
| 49 | } satisfies ElectronApp.ElectronAppShape); | |
| 49 | } satisfies ElectronApp.ElectronApp["Service"]); | |
| 50 | 50 | |
| 51 | 51 | const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, { |
| 52 | 52 | pickFolder: () => Effect.succeed(Option.none()), |
| 53 | 53 | confirm: () => Effect.succeed(false), |
| 54 | 54 | showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }), |
| 55 | 55 | showErrorBox: () => Effect.void, |
| 56 | } satisfies ElectronDialog.ElectronDialogShape); | |
| 56 | } satisfies ElectronDialog.ElectronDialog["Service"]); | |
| 57 | 57 | |
| 58 | 58 | const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, { |
| 59 | 59 | getState: Effect.die("unexpected getState"), |
| ... | ... | @@ -86,7 +86,7 @@ const makeElectronMenuLayer = ( |
| 86 | 86 | Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid), |
| 87 | 87 | popupTemplate: () => Effect.void, |
| 88 | 88 | showContextMenu: () => Effect.succeed(Option.none()), |
| 89 | } satisfies ElectronMenu.ElectronMenuShape); | |
| 89 | } satisfies ElectronMenu.ElectronMenu["Service"]); | |
| 90 | 90 | |
| 91 | 91 | describe("DesktopApplicationMenu", () => { |
| 92 | 92 | it.effect("installs the native menu and routes Settings through DesktopWindow", () => |
apps/desktop/src/window/DesktopApplicationMenu.ts+3-3| ... | ... | @@ -6,7 +6,7 @@ import * as Option from "effect/Option"; |
| 6 | 6 | |
| 7 | 7 | import type * as Electron from "electron"; |
| 8 | 8 | |
| 9 | import * as DesktopObservability from "../app/DesktopObservability.ts"; | |
| 9 | import { makeComponentLogger } from "../app/DesktopObservability.ts"; | |
| 10 | 10 | import * as ElectronApp from "../electron/ElectronApp.ts"; |
| 11 | 11 | import * as ElectronDialog from "../electron/ElectronDialog.ts"; |
| 12 | 12 | import * as ElectronMenu from "../electron/ElectronMenu.ts"; |
| ... | ... | @@ -26,9 +26,9 @@ type DesktopApplicationMenuRuntimeServices = |
| 26 | 26 | | DesktopWindow.DesktopWindow |
| 27 | 27 | | ElectronDialog.ElectronDialog; |
| 28 | 28 | |
| 29 | const { logInfo: logUpdaterInfo } = DesktopObservability.makeComponentLogger("desktop-updater"); | |
| 29 | const { logInfo: logUpdaterInfo } = makeComponentLogger("desktop-updater"); | |
| 30 | 30 | |
| 31 | const { logError: logMenuError } = DesktopObservability.makeComponentLogger("desktop-menu"); | |
| 31 | const { logError: logMenuError } = makeComponentLogger("desktop-menu"); | |
| 32 | 32 | |
| 33 | 33 | const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* ( |
| 34 | 34 | action: string, |
apps/desktop/src/window/DesktopWindow.ts+6-6| ... | ... | @@ -8,15 +8,15 @@ import type * as Electron from "electron"; |
| 8 | 8 | |
| 9 | 9 | import * as DesktopAssets from "../app/DesktopAssets.ts"; |
| 10 | 10 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
| 11 | import * as DesktopObservability from "../app/DesktopObservability.ts"; | |
| 11 | import { makeComponentLogger } from "../app/DesktopObservability.ts"; | |
| 12 | 12 | import * as DesktopState from "../app/DesktopState.ts"; |
| 13 | import * as PreviewManager from "../preview/Manager.ts"; | |
| 14 | 13 | import * as ElectronMenu from "../electron/ElectronMenu.ts"; |
| 14 | import { getDesktopUrl } from "../electron/ElectronProtocol.ts"; | |
| 15 | 15 | import * as ElectronShell from "../electron/ElectronShell.ts"; |
| 16 | 16 | import * as ElectronTheme from "../electron/ElectronTheme.ts"; |
| 17 | 17 | import * as ElectronWindow from "../electron/ElectronWindow.ts"; |
| 18 | import { getDesktopUrl } from "../electron/ElectronProtocol.ts"; | |
| 19 | import * as IpcChannels from "../ipc/channels.ts"; | |
| 18 | import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts"; | |
| 19 | import * as PreviewManager from "../preview/Manager.ts"; | |
| 20 | 20 | |
| 21 | 21 | const TITLEBAR_HEIGHT = 40; |
| 22 | 22 | const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux |
| ... | ... | @@ -57,7 +57,7 @@ export class DesktopWindow extends Context.Service< |
| 57 | 57 | >()("@t3tools/desktop/window/DesktopWindow") {} |
| 58 | 58 | |
| 59 | 59 | const { logInfo: logWindowInfo, logWarning: logWindowWarning } = |
| 60 | DesktopObservability.makeComponentLogger("desktop-window"); | |
| 60 | makeComponentLogger("desktop-window"); | |
| 61 | 61 | |
| 62 | 62 | function getIconOption( |
| 63 | 63 | iconPaths: DesktopAssets.DesktopIconPaths, |
| ... | ... | @@ -380,7 +380,7 @@ export const make = Effect.gen(function* () { |
| 380 | 380 | |
| 381 | 381 | const send = () => { |
| 382 | 382 | if (targetWindow.isDestroyed()) return; |
| 383 | targetWindow.webContents.send(IpcChannels.MENU_ACTION_CHANNEL, action); | |
| 383 | targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); | |
| 384 | 384 | void runPromise(electronWindow.reveal(targetWindow)); |
| 385 | 385 | }; |
| 386 | 386 |