import type { DesktopAppBranding, DesktopAppStageLabel, DesktopRuntimeArch, DesktopRuntimeInfo, } from "@t3tools/contracts"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; import * as DesktopConfig from "./DesktopConfig.ts"; import { resolveLinuxDesktopEntryName } from "./DesktopEarlyElectronStartup.ts"; import { resolveDesktopBaseDir, resolveDesktopStateDir } from "./DesktopStatePaths.ts"; import { isNightlyDesktopVersion } from "../updates/updateChannels.ts"; export interface MakeDesktopEnvironmentInput { readonly dirname: string; readonly homeDirectory: string; readonly platform: NodeJS.Platform; readonly processArch: string; readonly appVersion: string; readonly appPath: string; readonly isPackaged: boolean; readonly resourcesPath: string; readonly runningUnderArm64Translation: boolean; } export class DesktopEnvironment extends Context.Service< DesktopEnvironment, { readonly path: Path.Path; readonly dirname: string; readonly platform: NodeJS.Platform; readonly processArch: string; readonly isPackaged: boolean; readonly isDevelopment: boolean; readonly appVersion: string; readonly appPath: string; readonly resourcesPath: string; readonly homeDirectory: string; readonly appDataDirectory: string; readonly baseDir: string; readonly stateDir: string; readonly desktopSettingsPath: string; readonly clientSettingsPath: string; readonly savedEnvironmentRegistryPath: string; readonly serverSettingsPath: string; readonly logDir: string; readonly browserArtifactsDir: string; readonly rootDir: string; readonly appRoot: string; // Root of the tree containing apps/server/dist and node_modules for the // backend. Equals appRoot everywhere except packaged Windows, where the // server tree ships as the resources/server.asar sidecar (see // scripts/build-desktop-artifact.ts) that the asar-aware // ELECTRON_RUN_AS_NODE primary reads in place and the WSL backend // extracts on demand (see DesktopWslServerTree). readonly serverRoot: string; readonly backendEntryPath: string; readonly backendCwd: string; readonly preloadPath: string; readonly appUpdateYmlPath: string; readonly devServerUrl: Option.Option; readonly devRemoteT3ServerEntryPath: Option.Option; readonly configuredBackendPort: Option.Option; readonly commitHashOverride: Option.Option; readonly otlpTracesUrl: Option.Option; readonly otlpExportIntervalMs: number; readonly branding: DesktopAppBranding; readonly displayName: string; readonly appUserModelId: string; readonly linuxDesktopEntryName: string; readonly linuxWmClass: string; readonly linuxApplicationsDir: string; readonly appImagePath: Option.Option; readonly userDataDirName: string; readonly legacyUserDataDirName: string; readonly defaultDesktopSettings: DesktopAppSettings.DesktopSettings; readonly runtimeInfo: DesktopRuntimeInfo; readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option; readonly resolveResourcePathCandidates: (fileName: string) => readonly string[]; } >()("@t3tools/desktop/app/DesktopEnvironment") {} const APP_BASE_NAME = "T3 Code"; function resolveDesktopAppStageLabel(input: { readonly isDevelopment: boolean; readonly appVersion: string; }): DesktopAppStageLabel { if (input.isDevelopment) { return "Dev"; } return isNightlyDesktopVersion(input.appVersion) ? "Nightly" : "Alpha"; } export function resolveDesktopAppBranding(input: { readonly isDevelopment: boolean; readonly appVersion: string; }): DesktopAppBranding { const stageLabel = resolveDesktopAppStageLabel(input); return { baseName: APP_BASE_NAME, stageLabel, displayName: `${APP_BASE_NAME} (${stageLabel})`, }; } function normalizeDesktopArch(arch: string): DesktopRuntimeArch { if (arch === "arm64") return "arm64"; if (arch === "x64") return "x64"; return "other"; } function resolveDesktopRuntimeInfo(input: { readonly platform: NodeJS.Platform; readonly processArch: string; readonly runningUnderArm64Translation: boolean; }): DesktopRuntimeInfo { const appArch = normalizeDesktopArch(input.processArch); if (input.platform !== "darwin") { return { hostArch: appArch, appArch, runningUnderArm64Translation: false, }; } const hostArch = appArch === "arm64" || input.runningUnderArm64Translation ? "arm64" : appArch; return { hostArch, appArch, runningUnderArm64Translation: input.runningUnderArm64Translation, }; } const make = Effect.fn("desktop.environment.make")(function* ( input: MakeDesktopEnvironmentInput, ): Effect.fn.Return { const path = yield* Path.Path; const config = yield* DesktopConfig.DesktopConfig; const homeDirectory = input.homeDirectory; const devServerUrl = config.devServerUrl; const isDevelopment = Option.isSome(devServerUrl); const appDataDirectory = input.platform === "win32" ? Option.getOrElse(config.appDataDirectory, () => path.join(homeDirectory, "AppData", "Roaming"), ) : input.platform === "darwin" ? path.join(homeDirectory, "Library", "Application Support") : Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config")); const baseDir = resolveDesktopBaseDir({ homeDirectory, joinPath: path.join, t3Home: config.t3Home, }); const rootDir = path.resolve(input.dirname, "../../.."); const appRoot = input.isPackaged ? input.appPath : rootDir; const serverRoot = input.isPackaged && input.platform === "win32" ? path.join(input.resourcesPath, "server.asar") : appRoot; const branding = resolveDesktopAppBranding({ isDevelopment, appVersion: input.appVersion, }); const displayName = branding.displayName; const stateDir = resolveDesktopStateDir({ baseDir, isDevelopment, joinPath: path.join, t3Home: config.t3Home, }); const userDataDirName = isDevelopment ? "t3code-dev" : "t3code"; const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const linuxApplicationsDir = path.join( Option.getOrElse(config.xdgDataHome, () => path.join(homeDirectory, ".local", "share")), "applications", ); const resourcesPath = input.resourcesPath; return DesktopEnvironment.of({ path, dirname: input.dirname, platform: input.platform, processArch: input.processArch, isPackaged: input.isPackaged, isDevelopment, appVersion: input.appVersion, appPath: input.appPath, resourcesPath, homeDirectory, appDataDirectory, baseDir, stateDir, desktopSettingsPath: path.join(stateDir, "desktop-settings.json"), clientSettingsPath: path.join(stateDir, "client-settings.json"), savedEnvironmentRegistryPath: path.join(stateDir, "saved-environments.json"), serverSettingsPath: path.join(stateDir, "settings.json"), logDir: path.join(stateDir, "logs"), browserArtifactsDir: path.join(stateDir, "browser-artifacts"), rootDir, appRoot, serverRoot, backendEntryPath: path.join(serverRoot, "apps/server/dist/bin.mjs"), backendCwd: input.isPackaged ? homeDirectory : appRoot, preloadPath: path.join(input.dirname, "preload.cjs"), appUpdateYmlPath: input.isPackaged ? path.join(resourcesPath, "app-update.yml") : path.join(input.appPath, "dev-app-update.yml"), devServerUrl, devRemoteT3ServerEntryPath: config.devRemoteT3ServerEntryPath, configuredBackendPort: config.configuredBackendPort, commitHashOverride: config.commitHashOverride, otlpTracesUrl: config.otlpTracesUrl, otlpExportIntervalMs: config.otlpExportIntervalMs, branding, displayName, appUserModelId: Option.getOrElse(config.appUserModelIdOverride, () => isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code", ), linuxDesktopEntryName: resolveLinuxDesktopEntryName(isDevelopment), linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", linuxApplicationsDir, appImagePath: config.appImagePath, userDataDirName, legacyUserDataDirName, defaultDesktopSettings: DesktopAppSettings.resolveDefaultDesktopSettings(input.appVersion), runtimeInfo: resolveDesktopRuntimeInfo({ platform: input.platform, processArch: input.processArch, runningUnderArm64Translation: input.runningUnderArm64Translation, }), resolvePickFolderDefaultPath: (rawOptions) => { if (typeof rawOptions !== "object" || rawOptions === null) { return Option.none(); } const { initialPath } = rawOptions as { initialPath?: unknown }; if (typeof initialPath !== "string") { return Option.none(); } const trimmedPath = initialPath.trim(); if (trimmedPath.length === 0) { return Option.none(); } if (trimmedPath === "~") { return Option.some(homeDirectory); } if (trimmedPath.startsWith("~/") || trimmedPath.startsWith("~\\")) { return Option.some(path.join(homeDirectory, trimmedPath.slice(2))); } return Option.some(path.resolve(trimmedPath)); }, resolveResourcePathCandidates: (fileName) => [ path.join(input.dirname, "../resources", fileName), path.join(input.dirname, "../prod-resources", fileName), path.join(resourcesPath, "resources", fileName), path.join(resourcesPath, fileName), ], }); }); export const layer = (input: MakeDesktopEnvironmentInput) => Layer.effect(DesktopEnvironment, make(input));