593 lines · 21.4 KB · TypeScript
RawDownload
import {  DesktopServerExposureModeSchema,  DesktopUpdateChannelSchema,  type DesktopServerExposureMode,  type DesktopUpdateChannel,} from "@t3tools/contracts";import { fromLenientJson } from "@t3tools/shared/schemaJson";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";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 Path from "effect/Path";import * as Schema from "effect/Schema";import * as SynchronizedRef from "effect/SynchronizedRef"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import {  DEFAULT_LINUX_PASSWORD_STORE,  normalizeLinuxPasswordStorePreference,  type LinuxPasswordStorePreference,} from "../linuxSecretStorage.ts";import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts";import { isValidDistroName } from "../wsl/wslPathParsing.ts"; export interface DesktopSettings {  readonly linuxPasswordStore: LinuxPasswordStorePreference;  readonly mainWindowBounds: DesktopWindowBounds | null;  readonly mainWindowMaximized: boolean;  readonly serverExposureMode: DesktopServerExposureMode;  readonly tailscaleServeEnabled: boolean;  readonly tailscaleServePort: number;  readonly updateChannel: DesktopUpdateChannel;  readonly updateChannelConfiguredByUser: boolean;  // Was a "local" | "wsl" swap mode in an earlier iteration of the WSL  // integration. We now run Windows and WSL backends side by side, so the  // setting is just whether the WSL backend should be running alongside the  // primary. Persisted documents that still carry the legacy `wslMode: "wsl"`  // value are migrated to `wslBackendEnabled: true` on load.  readonly wslBackendEnabled: boolean;  readonly wslDistro: string | null;  // When true (and wslBackendEnabled is also true) the desktop runs only  // the WSL backend as the primary, and the Windows-side Node backend is  // not started. Designed for users who develop entirely inside WSL and  // don't want a second backend process running. Defaults to false so  // existing setups stay on the parallel-backends behavior. Changing  // this requires a desktop restart because the pool's primary spec is  // chosen once at layer init.  readonly wslOnly: boolean;} export interface DesktopSettingsChange {  readonly settings: DesktopSettings;  readonly changed: boolean;} const DEFAULT_TAILSCALE_SERVE_PORT = 443;const MIN_MAIN_WINDOW_SIZE = {  width: 840,  height: 620,} as const;export const DesktopWindowBoundsSchema = Schema.Struct({  x: Schema.Int,  y: Schema.Int,  width: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.width)),  height: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.height)),});export type DesktopWindowBounds = typeof DesktopWindowBoundsSchema.Type;export const DEFAULT_MAIN_WINDOW_SIZE = {  width: 1100,  height: 780,} as const; export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {  linuxPasswordStore: DEFAULT_LINUX_PASSWORD_STORE,  mainWindowBounds: null,  mainWindowMaximized: false,  serverExposureMode: "local-only",  tailscaleServeEnabled: false,  tailscaleServePort: DEFAULT_TAILSCALE_SERVE_PORT,  updateChannel: "latest",  updateChannelConfiguredByUser: false,  wslBackendEnabled: false,  wslDistro: null,  wslOnly: false,}; const DesktopWindowBoundsDocument = Schema.Struct({  x: Schema.Number,  y: Schema.Number,  width: Schema.Number,  height: Schema.Number,}); const DesktopSettingsDocument = Schema.Struct({  linuxPasswordStore: Schema.optionalKey(Schema.Unknown),  mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)),  mainWindowMaximized: Schema.optionalKey(Schema.Boolean),  serverExposureMode: Schema.optionalKey(DesktopServerExposureModeSchema),  tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),  tailscaleServePort: Schema.optionalKey(Schema.Number),  updateChannel: Schema.optionalKey(DesktopUpdateChannelSchema),  updateChannelConfiguredByUser: Schema.optionalKey(Schema.Boolean),  // Newer form of the WSL toggle. `wslMode` is still accepted on load so  // existing on-disk settings keep working; on the next persist we write the  // new boolean and the legacy key drops out.  wslBackendEnabled: Schema.optionalKey(Schema.Boolean),  wslMode: Schema.optionalKey(Schema.Literals(["local", "wsl"])),  wslDistro: Schema.optionalKey(Schema.NullOr(Schema.String)),  wslOnly: Schema.optionalKey(Schema.Boolean),}); type DesktopSettingsDocument = typeof DesktopSettingsDocument.Type;type Mutable<T> = { -readonly [K in keyof T]: T[K] }; const DesktopSettingsJson = fromLenientJson(DesktopSettingsDocument);const decodeDesktopSettingsJson = Schema.decodeEffect(DesktopSettingsJson);const encodeDesktopSettingsJson = Schema.encodeEffect(DesktopSettingsJson);const decodeDesktopWindowBounds = Schema.decodeUnknownOption(DesktopWindowBoundsSchema);const desktopWindowBoundsEquivalence = Schema.toEquivalence(DesktopWindowBoundsSchema); const settingsChange = (settings: DesktopSettings, changed: boolean): DesktopSettingsChange => ({  settings,  changed,}); const DesktopSettingsWriteOperation = Schema.Literals([  "create-temporary-file-name",  "encode-document",  "create-directory",  "write-temporary-file",  "replace-settings-file",]);type DesktopSettingsWriteOperation = typeof DesktopSettingsWriteOperation.Type; export class DesktopSettingsWriteError extends Schema.TaggedError<DesktopSettingsWriteError>()(  "DesktopSettingsWriteError",  {    operation: DesktopSettingsWriteOperation,    path: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Desktop settings write failed during ${this.operation} at ${this.path}.`;  }} export class DesktopAppSettings extends Context.Service<  DesktopAppSettings,  {    readonly load: Effect.Effect<DesktopSettings>;    readonly get: Effect.Effect<DesktopSettings>;    readonly setMainWindowBounds: (      bounds: DesktopWindowBounds,      isMaximized: boolean,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setServerExposureMode: (      mode: DesktopServerExposureMode,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setTailscaleServe: (input: {      readonly enabled: boolean;      readonly port: Option.Option<number>;    }) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setUpdateChannel: (      channel: DesktopUpdateChannel,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setWslBackendEnabled: (      enabled: boolean,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setWslDistro: (      distro: string | null,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly setWslOnly: (      enabled: boolean,    ) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;    readonly applyWslWindowsFallback: Effect.Effect<      DesktopSettingsChange,      DesktopSettingsWriteError    >;    readonly applyWslWindowsFallbackInMemory: Effect.Effect<DesktopSettingsChange>;  }>()("@t3tools/desktop/settings/DesktopAppSettings") {} export function resolveDefaultDesktopSettings(appVersion: string): DesktopSettings {  return {    ...DEFAULT_DESKTOP_SETTINGS,    updateChannel: resolveDefaultDesktopUpdateChannel(appVersion),  };} function normalizeTailscaleServePort(value: unknown): number {  return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65_535    ? value    : DEFAULT_TAILSCALE_SERVE_PORT;} function normalizeWslDistro(value: unknown): string | null {  return typeof value === "string" && isValidDistroName(value) ? value : null;} export function normalizeMainWindowBounds(value: unknown): DesktopWindowBounds | null {  return Option.getOrNull(decodeDesktopWindowBounds(value));} function normalizeDesktopSettingsDocument(  parsed: DesktopSettingsDocument,  appVersion: string,): DesktopSettings {  const defaultSettings = resolveDefaultDesktopSettings(appVersion);  const mainWindowBounds = normalizeMainWindowBounds(parsed.mainWindowBounds);  const parsedUpdateChannel = Option.fromNullishOr(parsed.updateChannel);  const isLegacySettings = parsed.updateChannelConfiguredByUser === undefined;  const updateChannelConfiguredByUser =    parsed.updateChannelConfiguredByUser === true ||    (isLegacySettings && Option.contains(parsedUpdateChannel, "nightly"));   // Newer form wins when both are present; otherwise fall back to the legacy  // `wslMode === "wsl"` signal so users coming off the swap-mode build keep  // their WSL backend enabled.  const wslBackendEnabled =    parsed.wslBackendEnabled === true ||    (parsed.wslBackendEnabled === undefined && parsed.wslMode === "wsl");   return {    linuxPasswordStore: normalizeLinuxPasswordStorePreference(parsed.linuxPasswordStore),    mainWindowBounds,    mainWindowMaximized: mainWindowBounds !== null && parsed.mainWindowMaximized === true,    serverExposureMode:      parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",    tailscaleServeEnabled: parsed.tailscaleServeEnabled === true,    tailscaleServePort: normalizeTailscaleServePort(parsed.tailscaleServePort),    updateChannel: updateChannelConfiguredByUser      ? Option.getOrElse(parsedUpdateChannel, () => defaultSettings.updateChannel)      : defaultSettings.updateChannel,    updateChannelConfiguredByUser,    wslBackendEnabled,    wslDistro: normalizeWslDistro(parsed.wslDistro),    wslOnly: parsed.wslOnly === true,  };} function toDesktopSettingsDocument(  settings: DesktopSettings,  defaults: DesktopSettings,): DesktopSettingsDocument {  const document: Mutable<DesktopSettingsDocument> = {};   if (settings.linuxPasswordStore !== defaults.linuxPasswordStore) {    document.linuxPasswordStore = settings.linuxPasswordStore;  }  if (settings.mainWindowBounds !== null) {    document.mainWindowBounds = settings.mainWindowBounds;  }  if (settings.mainWindowMaximized) {    document.mainWindowMaximized = true;  }  if (settings.serverExposureMode !== defaults.serverExposureMode) {    document.serverExposureMode = settings.serverExposureMode;  }  if (settings.tailscaleServeEnabled !== defaults.tailscaleServeEnabled) {    document.tailscaleServeEnabled = settings.tailscaleServeEnabled;  }  if (settings.tailscaleServePort !== defaults.tailscaleServePort) {    document.tailscaleServePort = settings.tailscaleServePort;  }  if (settings.updateChannel !== defaults.updateChannel) {    document.updateChannel = settings.updateChannel;  }  if (settings.updateChannelConfiguredByUser !== defaults.updateChannelConfiguredByUser) {    document.updateChannelConfiguredByUser = settings.updateChannelConfiguredByUser;  }  if (settings.wslBackendEnabled !== defaults.wslBackendEnabled) {    document.wslBackendEnabled = settings.wslBackendEnabled;  }  if (settings.wslDistro !== defaults.wslDistro) {    document.wslDistro = settings.wslDistro;  }  if (settings.wslOnly !== defaults.wslOnly) {    document.wslOnly = settings.wslOnly;  }   return document;} function setServerExposureMode(  settings: DesktopSettings,  requestedMode: DesktopServerExposureMode,): DesktopSettings {  return settings.serverExposureMode === requestedMode    ? settings    : {        ...settings,        serverExposureMode: requestedMode,      };} function setMainWindowBounds(  settings: DesktopSettings,  bounds: DesktopWindowBounds,  isMaximized: boolean,): DesktopSettings {  return settings.mainWindowBounds !== null &&    desktopWindowBoundsEquivalence(settings.mainWindowBounds, bounds) &&    settings.mainWindowMaximized === isMaximized    ? settings    : {        ...settings,        mainWindowBounds: bounds,        mainWindowMaximized: isMaximized,      };} function setTailscaleServe(  settings: DesktopSettings,  input: { readonly enabled: boolean; readonly port: Option.Option<number> },): DesktopSettings {  const port = Option.match(input.port, {    onNone: () => settings.tailscaleServePort,    onSome: normalizeTailscaleServePort,  });  return settings.tailscaleServeEnabled === input.enabled && settings.tailscaleServePort === port    ? settings    : {        ...settings,        tailscaleServeEnabled: input.enabled,        tailscaleServePort: port,      };} function setUpdateChannel(  settings: DesktopSettings,  requestedChannel: DesktopUpdateChannel,): DesktopSettings {  return settings.updateChannel === requestedChannel    ? settings    : {        ...settings,        updateChannel: requestedChannel,        updateChannelConfiguredByUser: true,      };} function setWslBackendEnabled(settings: DesktopSettings, enabled: boolean): DesktopSettings {  return settings.wslBackendEnabled === enabled    ? settings    : {        ...settings,        wslBackendEnabled: enabled,      };} function setWslDistro(settings: DesktopSettings, distro: string | null): DesktopSettings {  const normalized = normalizeWslDistro(distro);  return settings.wslDistro === normalized    ? settings    : {        ...settings,        wslDistro: normalized,      };} function setWslOnly(settings: DesktopSettings, enabled: boolean): DesktopSettings {  return settings.wslOnly === enabled    ? settings    : {        ...settings,        wslOnly: enabled,      };} function applyWslWindowsFallback(settings: DesktopSettings): DesktopSettings {  return setWslOnly(setWslBackendEnabled(settings, false), false);} function readSettings(  fileSystem: FileSystem.FileSystem,  settingsPath: string,  appVersion: string,): Effect.Effect<DesktopSettings> {  const defaultSettings = resolveDefaultDesktopSettings(appVersion);   return fileSystem.readFileString(settingsPath).pipe(    Effect.option,    Effect.flatMap(      Option.match({        onNone: () => Effect.succeed(defaultSettings),        onSome: (raw) =>          decodeDesktopSettingsJson(raw).pipe(            Effect.map((parsed) => normalizeDesktopSettingsDocument(parsed, appVersion)),            Effect.orElseSucceed(() => defaultSettings),          ),      }),    ),  );} const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (input: {  readonly fileSystem: FileSystem.FileSystem;  readonly path: Path.Path;  readonly settingsPath: string;  readonly settings: DesktopSettings;  readonly defaultSettings: DesktopSettings;  readonly suffix: string;}): Effect.fn.Return<void, DesktopSettingsWriteError> {  const directory = input.path.dirname(input.settingsPath);  const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;  const encoded = yield* encodeDesktopSettingsJson(    toDesktopSettingsDocument(input.settings, input.defaultSettings),  ).pipe(    Effect.mapError(      (cause) =>        new DesktopSettingsWriteError({          operation: "encode-document",          path: input.settingsPath,          cause,        }),    ),  );  yield* input.fileSystem.makeDirectory(directory, { recursive: true }).pipe(    Effect.mapError(      (cause) =>        new DesktopSettingsWriteError({          operation: "create-directory",          path: directory,          cause,        }),    ),  );  yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`).pipe(    Effect.mapError(      (cause) =>        new DesktopSettingsWriteError({          operation: "write-temporary-file",          path: tempPath,          cause,        }),    ),  );  yield* input.fileSystem.rename(tempPath, input.settingsPath).pipe(    Effect.mapError(      (cause) =>        new DesktopSettingsWriteError({          operation: "replace-settings-file",          path: input.settingsPath,          cause,        }),    ),  );}); /** @public Service construction is part of the canonical Effect module API. */export const make = Effect.gen(function* () {  const environment = yield* DesktopEnvironment.DesktopEnvironment;  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const crypto = yield* Crypto.Crypto;  const settingsRef = yield* SynchronizedRef.make(environment.defaultDesktopSettings);   const updateInMemory = (update: (settings: DesktopSettings) => DesktopSettings) =>    SynchronizedRef.modify(settingsRef, (settings) => {      const nextSettings = update(settings);      return [settingsChange(nextSettings, nextSettings !== settings), nextSettings] as const;    });   const persist = (    update: (settings: DesktopSettings) => DesktopSettings,  ): Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError> =>    SynchronizedRef.modifyEffect(settingsRef, (settings) => {      const nextSettings = update(settings);      if (nextSettings === settings) {        return Effect.succeed([settingsChange(settings, false), settings] as const);      }       return crypto.randomUUIDv4.pipe(        Effect.map((uuid) => uuid.replace(/-/g, "")),        Effect.mapError(          (cause) =>            new DesktopSettingsWriteError({              operation: "create-temporary-file-name",              path: environment.desktopSettingsPath,              cause,            }),        ),        Effect.flatMap((suffix) =>          writeSettings({            fileSystem,            path,            settingsPath: environment.desktopSettingsPath,            settings: nextSettings,            defaultSettings: environment.defaultDesktopSettings,            suffix,          }),        ),        Effect.as([settingsChange(nextSettings, true), nextSettings] as const),      );    });   return DesktopAppSettings.of({

Lines 1–500 of 593.