Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ function addScopedListener<Args extends ReadonlyArray<unknown>>(
}

const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")(
function* (): Effect.fn.Return<void, never, DesktopShutdown.DesktopShutdown> {
function* (): Effect.fn.Return<
void,
never,
DesktopShutdown.DesktopShutdown | DesktopWindow.DesktopWindow
> {
const shutdown = yield* DesktopShutdown.DesktopShutdown;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.flushMainWindowBounds;
yield* shutdown.request;
yield* shutdown.awaitComplete;
},
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function makePoolLayer(
showConnectingSplash: Effect.void,
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ describe("DesktopServerExposure", () => {
const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.fail(settingsFailure),
setTailscaleServe: () => Effect.fail(settingsFailure),
setUpdateChannel: () => Effect.die("unexpected update channel change"),
Expand Down
35 changes: 35 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopAppSettings from "./DesktopAppSettings.ts";

const DesktopSettingsPatch = Schema.Struct({
mainWindowBounds: Schema.optionalKey(
Schema.NullOr(
Schema.Struct({
x: Schema.Number,
y: Schema.Number,
width: Schema.Number,
height: Schema.Number,
}),
),
),
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
tailscaleServePort: Schema.optionalKey(Schema.Number),
Expand Down Expand Up @@ -91,6 +101,7 @@ describe("DesktopSettings", () => {
assert.deepEqual(
DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"),
{
mainWindowBounds: null,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -116,6 +127,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
Expand Down Expand Up @@ -215,10 +227,12 @@ describe("DesktopSettings", () => {
"serverExposureMode": "network-accessible",
"tailscaleServeEnabled": true,
"tailscaleServePort": 8443,
"mainWindowBounds": { "x": 120, "y": 80, "width": 1280, "height": 900 },
}\n`,
);

assert.deepEqual(yield* settings.load, {
mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 },
serverExposureMode: "network-accessible",
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
Expand All @@ -232,19 +246,37 @@ describe("DesktopSettings", () => {
),
);

it.effect("rejects window bounds that do not satisfy the domain schema", () =>
withSettings(
Effect.gen(function* () {
const settings = yield* DesktopAppSettings.DesktopAppSettings;
yield* writeSettingsPatch({
mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 },
serverExposureMode: "network-accessible",
});

const loaded = yield* settings.load;
assert.isNull(loaded.mainWindowBounds);
assert.equal(loaded.serverExposureMode, "network-accessible");
}),
),
);

it.effect("persists sparse desktop settings documents", () =>
withSettings(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const fileSystem = yield* FileSystem.FileSystem;
const settings = yield* DesktopAppSettings.DesktopAppSettings;

yield* settings.setMainWindowBounds({ x: -1200, y: 40, width: 1440, height: 960 });
yield* settings.setServerExposureMode("network-accessible");

const persisted = yield* decodeDesktopSettingsPatch(
yield* fileSystem.readFileString(environment.desktopSettingsPath),
);
assert.deepEqual(persisted, {
mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 },
serverExposureMode: "network-accessible",
} satisfies typeof DesktopSettingsPatch.Type);
}),
Expand All @@ -261,6 +293,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -286,6 +319,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: 443,
Expand All @@ -310,6 +344,7 @@ describe("DesktopSettings", () => {
});

assert.deepEqual(yield* settings.load, {
mainWindowBounds: null,
serverExposureMode: "local-only",
tailscaleServeEnabled: true,
tailscaleServePort: 443,
Expand Down
64 changes: 64 additions & 0 deletions apps/desktop/src/settings/DesktopAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts
import { isValidDistroName } from "../wsl/wslPathParsing.ts";

export interface DesktopSettings {
readonly mainWindowBounds: DesktopWindowBounds | null;
readonly serverExposureMode: DesktopServerExposureMode;
readonly tailscaleServeEnabled: boolean;
readonly tailscaleServePort: number;
Expand Down Expand Up @@ -48,8 +49,24 @@ export interface DesktopSettingsChange {
}

export 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 = {
mainWindowBounds: null,
serverExposureMode: "local-only",
tailscaleServeEnabled: false,
tailscaleServePort: DEFAULT_TAILSCALE_SERVE_PORT,
Expand All @@ -60,7 +77,15 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
wslOnly: false,
};

const DesktopWindowBoundsDocument = Schema.Struct({
x: Schema.Number,
y: Schema.Number,
width: Schema.Number,
height: Schema.Number,
});

const DesktopSettingsDocument = Schema.Struct({
mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)),
serverExposureMode: Schema.optionalKey(DesktopServerExposureModeSchema),
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
tailscaleServePort: Schema.optionalKey(Schema.Number),
Expand All @@ -81,6 +106,8 @@ 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,
Expand Down Expand Up @@ -114,6 +141,9 @@ export class DesktopAppSettings extends Context.Service<
{
readonly load: Effect.Effect<DesktopSettings>;
readonly get: Effect.Effect<DesktopSettings>;
readonly setMainWindowBounds: (
bounds: DesktopWindowBounds,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
readonly setServerExposureMode: (
mode: DesktopServerExposureMode,
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
Expand Down Expand Up @@ -158,6 +188,10 @@ function normalizeWslDistro(value: unknown): string | null {
return typeof value === "string" && isValidDistroName(value) ? value : null;
}

function normalizeMainWindowBounds(value: unknown): DesktopWindowBounds | null {
return Option.getOrNull(decodeDesktopWindowBounds(value));
}

function normalizeDesktopSettingsDocument(
parsed: DesktopSettingsDocument,
appVersion: string,
Expand All @@ -177,6 +211,7 @@ function normalizeDesktopSettingsDocument(
(parsed.wslBackendEnabled === undefined && parsed.wslMode === "wsl");

return {
mainWindowBounds: normalizeMainWindowBounds(parsed.mainWindowBounds),
serverExposureMode:
parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",
tailscaleServeEnabled: parsed.tailscaleServeEnabled === true,
Expand All @@ -197,6 +232,9 @@ function toDesktopSettingsDocument(
): DesktopSettingsDocument {
const document: Mutable<DesktopSettingsDocument> = {};

if (settings.mainWindowBounds !== null) {
document.mainWindowBounds = settings.mainWindowBounds;
}
if (settings.serverExposureMode !== defaults.serverExposureMode) {
document.serverExposureMode = settings.serverExposureMode;
}
Expand Down Expand Up @@ -237,6 +275,19 @@ function setServerExposureMode(
};
}

function setMainWindowBounds(
settings: DesktopSettings,
bounds: DesktopWindowBounds,
): DesktopSettings {
return settings.mainWindowBounds !== null &&
desktopWindowBoundsEquivalence(settings.mainWindowBounds, bounds)
? settings
: {
...settings,
mainWindowBounds: bounds,
};
}

function setTailscaleServe(
settings: DesktopSettings,
input: { readonly enabled: boolean; readonly port: Option.Option<number> },
Expand Down Expand Up @@ -431,6 +482,17 @@ export const make = Effect.gen(function* () {
);
return yield* SynchronizedRef.setAndGet(settingsRef, settings);
}).pipe(Effect.withSpan("desktop.settings.load")),
setMainWindowBounds: (bounds) =>
persist((settings) => setMainWindowBounds(settings, bounds)).pipe(
Effect.withSpan("desktop.settings.setMainWindowBounds", {
attributes: {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
},
}),
),
setServerExposureMode: (mode) =>
persist((settings) => setServerExposureMode(settings, mode)).pipe(
Effect.withSpan("desktop.settings.setServerExposureMode", { attributes: { mode } }),
Expand Down Expand Up @@ -488,6 +550,8 @@ export const layerTest = (initialSettings: DesktopSettings = DEFAULT_DESKTOP_SET
return DesktopAppSettings.of({
get: SynchronizedRef.get(settingsRef),
load: SynchronizedRef.get(settingsRef),
setMainWindowBounds: (bounds) =>
update((settings) => setMainWindowBounds(settings, bounds)),
setServerExposureMode: (mode) =>
update((settings) => setServerExposureMode(settings, mode)),
setTailscaleServe: (input) => update((settings) => setTailscaleServe(settings, input)),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
? Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
setServerExposureMode: () => Effect.die("unexpected server exposure update"),
setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),
setUpdateChannel: () => Effect.fail(setUpdateChannelError),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
showConnectingSplash: Effect.void,
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
syncAppearance: Effect.void,
} satisfies DesktopWindow.DesktopWindow["Service"]);
Expand Down
Loading
Loading