Skip to content

Commit 7329819

Browse files
anirudhsamajuliusmarmingecodex
authored andcommitted
fix(desktop): preserve main window bounds (pingdotgg#3851)
Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> (cherry picked from commit 2c199aa)
1 parent b2df6a4 commit 7329819

13 files changed

Lines changed: 921 additions & 40 deletions

apps/desktop/src/app/DesktopLifecycle.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ function addScopedListener<Args extends ReadonlyArray<unknown>>(
7373
}
7474

7575
const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdownAndWait")(
76-
function* (): Effect.fn.Return<void, never, DesktopShutdown.DesktopShutdown> {
76+
function* (): Effect.fn.Return<
77+
void,
78+
never,
79+
DesktopShutdown.DesktopShutdown | DesktopWindow.DesktopWindow
80+
> {
7781
const shutdown = yield* DesktopShutdown.DesktopShutdown;
82+
const desktopWindow = yield* DesktopWindow.DesktopWindow;
83+
yield* desktopWindow.flushMainWindowBounds;
7884
yield* shutdown.request;
7985
yield* shutdown.awaitComplete;
8086
},

apps/desktop/src/backend/DesktopBackendPool.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function makePoolLayer(
7676
showConnectingSplash: Effect.void,
7777
handleBackendReady: () => Effect.void,
7878
handleBackendNotReady: Effect.void,
79+
flushMainWindowBounds: Effect.void,
7980
dispatchMenuAction: () => Effect.die("unexpected menu action"),
8081
syncAppearance: Effect.void,
8182
} satisfies DesktopWindow.DesktopWindow["Service"]),

apps/desktop/src/backend/DesktopServerExposure.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ describe("DesktopServerExposure", () => {
293293
const settingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
294294
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
295295
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
296+
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
296297
setServerExposureMode: () => Effect.fail(settingsFailure),
297298
setTailscaleServe: () => Effect.fail(settingsFailure),
298299
setUpdateChannel: () => Effect.die("unexpected update channel change"),

apps/desktop/src/settings/DesktopAppSettings.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
1111
import * as DesktopAppSettings from "./DesktopAppSettings.ts";
1212

1313
const DesktopSettingsPatch = Schema.Struct({
14+
mainWindowBounds: Schema.optionalKey(
15+
Schema.NullOr(
16+
Schema.Struct({
17+
x: Schema.Number,
18+
y: Schema.Number,
19+
width: Schema.Number,
20+
height: Schema.Number,
21+
}),
22+
),
23+
),
24+
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
1425
serverExposureMode: Schema.optionalKey(Schema.Literals(["local-only", "network-accessible"])),
1526
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
1627
tailscaleServePort: Schema.optionalKey(Schema.Number),
@@ -91,6 +102,8 @@ describe("DesktopSettings", () => {
91102
assert.deepEqual(
92103
DesktopAppSettings.resolveDefaultDesktopSettings("0.0.17-nightly.20260415.1"),
93104
{
105+
mainWindowBounds: null,
106+
mainWindowMaximized: false,
94107
serverExposureMode: "local-only",
95108
tailscaleServeEnabled: false,
96109
tailscaleServePort: 443,
@@ -116,6 +129,8 @@ describe("DesktopSettings", () => {
116129
});
117130

118131
assert.deepEqual(yield* settings.load, {
132+
mainWindowBounds: null,
133+
mainWindowMaximized: false,
119134
serverExposureMode: "network-accessible",
120135
tailscaleServeEnabled: true,
121136
tailscaleServePort: 8443,
@@ -215,10 +230,13 @@ describe("DesktopSettings", () => {
215230
"serverExposureMode": "network-accessible",
216231
"tailscaleServeEnabled": true,
217232
"tailscaleServePort": 8443,
233+
"mainWindowBounds": { "x": 120, "y": 80, "width": 1280, "height": 900 },
218234
}\n`,
219235
);
220236

221237
assert.deepEqual(yield* settings.load, {
238+
mainWindowBounds: { x: 120, y: 80, width: 1280, height: 900 },
239+
mainWindowMaximized: false,
222240
serverExposureMode: "network-accessible",
223241
tailscaleServeEnabled: true,
224242
tailscaleServePort: 8443,
@@ -232,19 +250,40 @@ describe("DesktopSettings", () => {
232250
),
233251
);
234252

253+
it.effect("rejects window bounds that do not satisfy the domain schema", () =>
254+
withSettings(
255+
Effect.gen(function* () {
256+
const settings = yield* DesktopAppSettings.DesktopAppSettings;
257+
yield* writeSettingsPatch({
258+
mainWindowBounds: { x: 10.5, y: 20, width: 839, height: 620 },
259+
mainWindowMaximized: true,
260+
serverExposureMode: "network-accessible",
261+
});
262+
263+
const loaded = yield* settings.load;
264+
assert.isNull(loaded.mainWindowBounds);
265+
assert.isFalse(loaded.mainWindowMaximized);
266+
assert.equal(loaded.serverExposureMode, "network-accessible");
267+
}),
268+
),
269+
);
270+
235271
it.effect("persists sparse desktop settings documents", () =>
236272
withSettings(
237273
Effect.gen(function* () {
238274
const environment = yield* DesktopEnvironment.DesktopEnvironment;
239275
const fileSystem = yield* FileSystem.FileSystem;
240276
const settings = yield* DesktopAppSettings.DesktopAppSettings;
241277

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

244281
const persisted = yield* decodeDesktopSettingsPatch(
245282
yield* fileSystem.readFileString(environment.desktopSettingsPath),
246283
);
247284
assert.deepEqual(persisted, {
285+
mainWindowBounds: { x: -1200, y: 40, width: 1440, height: 960 },
286+
mainWindowMaximized: true,
248287
serverExposureMode: "network-accessible",
249288
} satisfies typeof DesktopSettingsPatch.Type);
250289
}),
@@ -261,6 +300,8 @@ describe("DesktopSettings", () => {
261300
});
262301

263302
assert.deepEqual(yield* settings.load, {
303+
mainWindowBounds: null,
304+
mainWindowMaximized: false,
264305
serverExposureMode: "local-only",
265306
tailscaleServeEnabled: false,
266307
tailscaleServePort: 443,
@@ -286,6 +327,8 @@ describe("DesktopSettings", () => {
286327
});
287328

288329
assert.deepEqual(yield* settings.load, {
330+
mainWindowBounds: null,
331+
mainWindowMaximized: false,
289332
serverExposureMode: "local-only",
290333
tailscaleServeEnabled: false,
291334
tailscaleServePort: 443,
@@ -310,6 +353,8 @@ describe("DesktopSettings", () => {
310353
});
311354

312355
assert.deepEqual(yield* settings.load, {
356+
mainWindowBounds: null,
357+
mainWindowMaximized: false,
313358
serverExposureMode: "local-only",
314359
tailscaleServeEnabled: true,
315360
tailscaleServePort: 443,

apps/desktop/src/settings/DesktopAppSettings.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { resolveDefaultDesktopUpdateChannel } from "../updates/updateChannels.ts
2020
import { isValidDistroName } from "../wsl/wslPathParsing.ts";
2121

2222
export interface DesktopSettings {
23+
readonly mainWindowBounds: DesktopWindowBounds | null;
24+
readonly mainWindowMaximized: boolean;
2325
readonly serverExposureMode: DesktopServerExposureMode;
2426
readonly tailscaleServeEnabled: boolean;
2527
readonly tailscaleServePort: number;
@@ -48,8 +50,25 @@ export interface DesktopSettingsChange {
4850
}
4951

5052
export const DEFAULT_TAILSCALE_SERVE_PORT = 443;
53+
const MIN_MAIN_WINDOW_SIZE = {
54+
width: 840,
55+
height: 620,
56+
} as const;
57+
export const DesktopWindowBoundsSchema = Schema.Struct({
58+
x: Schema.Int,
59+
y: Schema.Int,
60+
width: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.width)),
61+
height: Schema.Int.check(Schema.isGreaterThanOrEqualTo(MIN_MAIN_WINDOW_SIZE.height)),
62+
});
63+
export type DesktopWindowBounds = typeof DesktopWindowBoundsSchema.Type;
64+
export const DEFAULT_MAIN_WINDOW_SIZE = {
65+
width: 1100,
66+
height: 780,
67+
} as const;
5168

5269
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
70+
mainWindowBounds: null,
71+
mainWindowMaximized: false,
5372
serverExposureMode: "local-only",
5473
tailscaleServeEnabled: false,
5574
tailscaleServePort: DEFAULT_TAILSCALE_SERVE_PORT,
@@ -60,7 +79,16 @@ export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
6079
wslOnly: false,
6180
};
6281

82+
const DesktopWindowBoundsDocument = Schema.Struct({
83+
x: Schema.Number,
84+
y: Schema.Number,
85+
width: Schema.Number,
86+
height: Schema.Number,
87+
});
88+
6389
const DesktopSettingsDocument = Schema.Struct({
90+
mainWindowBounds: Schema.optionalKey(Schema.NullOr(DesktopWindowBoundsDocument)),
91+
mainWindowMaximized: Schema.optionalKey(Schema.Boolean),
6492
serverExposureMode: Schema.optionalKey(DesktopServerExposureModeSchema),
6593
tailscaleServeEnabled: Schema.optionalKey(Schema.Boolean),
6694
tailscaleServePort: Schema.optionalKey(Schema.Number),
@@ -81,6 +109,8 @@ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
81109
const DesktopSettingsJson = fromLenientJson(DesktopSettingsDocument);
82110
const decodeDesktopSettingsJson = Schema.decodeEffect(DesktopSettingsJson);
83111
const encodeDesktopSettingsJson = Schema.encodeEffect(DesktopSettingsJson);
112+
const decodeDesktopWindowBounds = Schema.decodeUnknownOption(DesktopWindowBoundsSchema);
113+
const desktopWindowBoundsEquivalence = Schema.toEquivalence(DesktopWindowBoundsSchema);
84114

85115
const settingsChange = (settings: DesktopSettings, changed: boolean): DesktopSettingsChange => ({
86116
settings,
@@ -114,6 +144,10 @@ export class DesktopAppSettings extends Context.Service<
114144
{
115145
readonly load: Effect.Effect<DesktopSettings>;
116146
readonly get: Effect.Effect<DesktopSettings>;
147+
readonly setMainWindowBounds: (
148+
bounds: DesktopWindowBounds,
149+
isMaximized: boolean,
150+
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
117151
readonly setServerExposureMode: (
118152
mode: DesktopServerExposureMode,
119153
) => Effect.Effect<DesktopSettingsChange, DesktopSettingsWriteError>;
@@ -158,11 +192,16 @@ function normalizeWslDistro(value: unknown): string | null {
158192
return typeof value === "string" && isValidDistroName(value) ? value : null;
159193
}
160194

195+
export function normalizeMainWindowBounds(value: unknown): DesktopWindowBounds | null {
196+
return Option.getOrNull(decodeDesktopWindowBounds(value));
197+
}
198+
161199
function normalizeDesktopSettingsDocument(
162200
parsed: DesktopSettingsDocument,
163201
appVersion: string,
164202
): DesktopSettings {
165203
const defaultSettings = resolveDefaultDesktopSettings(appVersion);
204+
const mainWindowBounds = normalizeMainWindowBounds(parsed.mainWindowBounds);
166205
const parsedUpdateChannel = Option.fromNullishOr(parsed.updateChannel);
167206
const isLegacySettings = parsed.updateChannelConfiguredByUser === undefined;
168207
const updateChannelConfiguredByUser =
@@ -177,6 +216,8 @@ function normalizeDesktopSettingsDocument(
177216
(parsed.wslBackendEnabled === undefined && parsed.wslMode === "wsl");
178217

179218
return {
219+
mainWindowBounds,
220+
mainWindowMaximized: mainWindowBounds !== null && parsed.mainWindowMaximized === true,
180221
serverExposureMode:
181222
parsed.serverExposureMode === "network-accessible" ? "network-accessible" : "local-only",
182223
tailscaleServeEnabled: parsed.tailscaleServeEnabled === true,
@@ -197,6 +238,12 @@ function toDesktopSettingsDocument(
197238
): DesktopSettingsDocument {
198239
const document: Mutable<DesktopSettingsDocument> = {};
199240

241+
if (settings.mainWindowBounds !== null) {
242+
document.mainWindowBounds = settings.mainWindowBounds;
243+
}
244+
if (settings.mainWindowMaximized) {
245+
document.mainWindowMaximized = true;
246+
}
200247
if (settings.serverExposureMode !== defaults.serverExposureMode) {
201248
document.serverExposureMode = settings.serverExposureMode;
202249
}
@@ -237,6 +284,22 @@ function setServerExposureMode(
237284
};
238285
}
239286

287+
function setMainWindowBounds(
288+
settings: DesktopSettings,
289+
bounds: DesktopWindowBounds,
290+
isMaximized: boolean,
291+
): DesktopSettings {
292+
return settings.mainWindowBounds !== null &&
293+
desktopWindowBoundsEquivalence(settings.mainWindowBounds, bounds) &&
294+
settings.mainWindowMaximized === isMaximized
295+
? settings
296+
: {
297+
...settings,
298+
mainWindowBounds: bounds,
299+
mainWindowMaximized: isMaximized,
300+
};
301+
}
302+
240303
function setTailscaleServe(
241304
settings: DesktopSettings,
242305
input: { readonly enabled: boolean; readonly port: Option.Option<number> },
@@ -431,6 +494,18 @@ export const make = Effect.gen(function* () {
431494
);
432495
return yield* SynchronizedRef.setAndGet(settingsRef, settings);
433496
}).pipe(Effect.withSpan("desktop.settings.load")),
497+
setMainWindowBounds: (bounds, isMaximized) =>
498+
persist((settings) => setMainWindowBounds(settings, bounds, isMaximized)).pipe(
499+
Effect.withSpan("desktop.settings.setMainWindowBounds", {
500+
attributes: {
501+
x: bounds.x,
502+
y: bounds.y,
503+
width: bounds.width,
504+
height: bounds.height,
505+
isMaximized,
506+
},
507+
}),
508+
),
434509
setServerExposureMode: (mode) =>
435510
persist((settings) => setServerExposureMode(settings, mode)).pipe(
436511
Effect.withSpan("desktop.settings.setServerExposureMode", { attributes: { mode } }),
@@ -488,6 +563,8 @@ export const layerTest = (initialSettings: DesktopSettings = DEFAULT_DESKTOP_SET
488563
return DesktopAppSettings.of({
489564
get: SynchronizedRef.get(settingsRef),
490565
load: SynchronizedRef.get(settingsRef),
566+
setMainWindowBounds: (bounds, isMaximized) =>
567+
update((settings) => setMainWindowBounds(settings, bounds, isMaximized)),
491568
setServerExposureMode: (mode) =>
492569
update((settings) => setServerExposureMode(settings, mode)),
493570
setTailscaleServe: (input) => update((settings) => setTailscaleServe(settings, input)),

apps/desktop/src/updates/DesktopUpdates.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
158158
? Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
159159
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
160160
load: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
161+
setMainWindowBounds: () => Effect.die("unexpected main window bounds update"),
161162
setServerExposureMode: () => Effect.die("unexpected server exposure update"),
162163
setTailscaleServe: () => Effect.die("unexpected Tailscale Serve update"),
163164
setUpdateChannel: () => Effect.fail(setUpdateChannelError),

apps/desktop/src/window/DesktopApplicationMenu.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
7676
showConnectingSplash: Effect.void,
7777
handleBackendReady: () => Effect.void,
7878
handleBackendNotReady: Effect.void,
79+
flushMainWindowBounds: Effect.void,
7980
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
8081
syncAppearance: Effect.void,
8182
} satisfies DesktopWindow.DesktopWindow["Service"]);

0 commit comments

Comments
 (0)