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