import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as ElectronApp from "../electron/ElectronApp.ts"; import * as DesktopAssets from "./DesktopAssets.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i; const COMMIT_HASH_DISPLAY_LENGTH = 12; const AppPackageMetadata = Schema.Struct({ t3codeCommitHash: Schema.optional(Schema.String), }); const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata)); export class DesktopUserDataPathResolutionError extends Schema.TaggedError()( "DesktopUserDataPathResolutionError", { legacyPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to inspect legacy desktop user-data path at "${this.legacyPath}".`; } } export class DesktopAppIdentity extends Context.Service< DesktopAppIdentity, { readonly resolveUserDataPath: Effect.Effect; readonly configure: Effect.Effect; } >()("@t3tools/desktop/app/DesktopAppIdentity") {} const normalizeCommitHash = (value: string): Option.Option => { const trimmed = value.trim(); return COMMIT_HASH_PATTERN.test(trimmed) ? Option.some(trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase()) : Option.none(); }; export const resolveUserDataPath = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const legacyPath = environment.path.join( environment.appDataDirectory, environment.legacyUserDataDirName, ); const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe( Effect.mapError( (cause) => new DesktopUserDataPathResolutionError({ legacyPath, cause, }), ), ); return legacyPathExists ? legacyPath : environment.path.join(environment.appDataDirectory, environment.userDataDirName); }).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath")); /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const assets = yield* DesktopAssets.DesktopAssets; const electronApp = yield* ElectronApp.ElectronApp; const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const commitHashCache = yield* Ref.make>>(Option.none()); const resolveEmbeddedCommitHash = Effect.gen(function* () { const packageJsonPath = environment.path.join(environment.appRoot, "package.json"); const raw = yield* fileSystem.readFileString(packageJsonPath).pipe(Effect.option); return yield* Option.match(raw, { onNone: () => Effect.succeed(Option.none()), onSome: (value) => decodeAppPackageMetadata(value).pipe( Effect.map((parsed) => Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)), ), Effect.orElseSucceed(() => Option.none()), ), }); }); const resolveAboutCommitHash = Effect.gen(function* () { const cached = yield* Ref.get(commitHashCache); if (Option.isSome(cached)) { return cached.value; } const override = Option.flatMap(environment.commitHashOverride, normalizeCommitHash); if (Option.isSome(override)) { yield* Ref.set(commitHashCache, Option.some(override)); return override; } if (!environment.isPackaged) { const empty = Option.none(); yield* Ref.set(commitHashCache, Option.some(empty)); return empty; } const commitHash = yield* resolveEmbeddedCommitHash; yield* Ref.set(commitHashCache, Option.some(commitHash)); return commitHash; }); const userDataPath = resolveUserDataPath.pipe( Effect.provide( yield* Effect.context(), ), ); const configure = Effect.gen(function* () { const commitHash = yield* resolveAboutCommitHash; yield* electronApp.setName(environment.displayName); yield* electronApp.setAboutPanelOptions({ applicationName: environment.displayName, applicationVersion: environment.appVersion, version: Option.getOrElse(commitHash, () => "unknown"), }); if (environment.platform === "win32") { yield* electronApp.setAppUserModelId(environment.appUserModelId); } // Unpackaged runs only. A packaged bundle already carries its icon in // Info.plist, so setting the dock tile again changes nothing except to // overwrite a custom icon the user attached to the app themselves. if (environment.platform === "darwin" && !environment.isPackaged) { const iconPaths = yield* assets.iconPaths; yield* Option.match(iconPaths.png, { onNone: () => Effect.void, onSome: electronApp.setDockIcon, }); } }).pipe(Effect.withSpan("desktop.appIdentity.configure")); return DesktopAppIdentity.of({ resolveUserDataPath: userDataPath, configure, }); }); export const layer = Layer.effect(DesktopAppIdentity, make);