Skip to content

Commit f8e5d52

Browse files
authored
feat: window size persistence (#1102)
1 parent fafb118 commit f8e5d52

2 files changed

Lines changed: 106 additions & 5 deletions

File tree

apps/twig/src/main/utils/store.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ interface RendererStoreSchema {
3232
[key: string]: string;
3333
}
3434

35+
export interface WindowStateSchema {
36+
x: number | undefined;
37+
y: number | undefined;
38+
width: number;
39+
height: number;
40+
isMaximized: boolean;
41+
}
42+
3543
const schema = {
3644
folders: {
3745
type: "array" as const,
@@ -98,6 +106,18 @@ export const archiveStore = new Store<ArchiveStoreSchema>({
98106
defaults: { archivedTasks: [] },
99107
});
100108

109+
export const windowStateStore = new Store<WindowStateSchema>({
110+
name: "window-state",
111+
cwd: app.getPath("userData"),
112+
defaults: {
113+
x: undefined,
114+
y: undefined,
115+
width: 1200,
116+
height: 600,
117+
isMaximized: true,
118+
},
119+
});
120+
101121
const log = logger.scope("store");
102122

103123
interface LegacyTaskAssociation {
@@ -210,4 +230,5 @@ export async function clearAllStoreData(): Promise<void> {
210230
foldersStore.clear();
211231
rendererStore.clear();
212232
archiveStore.clear();
233+
windowStateStore.clear();
213234
}

apps/twig/src/main/window.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,61 @@
11
import path from "node:path";
22
import { fileURLToPath } from "node:url";
33
import { createIPCHandler } from "@posthog/electron-trpc/main";
4-
import { app, BrowserWindow, shell } from "electron";
4+
import { app, BrowserWindow, screen, shell } from "electron";
55
import { buildApplicationMenu } from "./menu.js";
66
import { setMainWindowGetter } from "./trpc/context.js";
77
import { trpcRouter } from "./trpc/router.js";
8+
import { type WindowStateSchema, windowStateStore } from "./utils/store.js";
89

910
declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string | undefined;
1011
declare const MAIN_WINDOW_VITE_NAME: string;
1112

1213
const __filename = fileURLToPath(import.meta.url);
1314
const __dirname = path.dirname(__filename);
1415

16+
function isPositionOnScreen(x: number, y: number): boolean {
17+
const displays = screen.getAllDisplays();
18+
return displays.some((display) => {
19+
const { x: dx, y: dy, width, height } = display.bounds;
20+
return x >= dx && x < dx + width && y >= dy && y < dy + height;
21+
});
22+
}
23+
24+
function getSavedWindowState(): WindowStateSchema {
25+
const state = {
26+
x: windowStateStore.get("x"),
27+
y: windowStateStore.get("y"),
28+
width: windowStateStore.get("width", 1200),
29+
height: windowStateStore.get("height", 600),
30+
isMaximized: windowStateStore.get("isMaximized", true),
31+
};
32+
33+
// Validate position is still on a connected display
34+
if (state.x !== undefined && state.y !== undefined) {
35+
if (!isPositionOnScreen(state.x, state.y)) {
36+
state.x = undefined;
37+
state.y = undefined;
38+
}
39+
}
40+
41+
return state;
42+
}
43+
44+
function saveWindowState(window: BrowserWindow): void {
45+
const isMaximized = window.isMaximized();
46+
windowStateStore.set("isMaximized", isMaximized);
47+
48+
// Only save bounds when not maximized, so restoring from maximized
49+
// gives the user their previous windowed size/position
50+
if (!isMaximized) {
51+
const bounds = window.getBounds();
52+
windowStateStore.set("x", bounds.x);
53+
windowStateStore.set("y", bounds.y);
54+
windowStateStore.set("width", bounds.width);
55+
windowStateStore.set("height", bounds.height);
56+
}
57+
}
58+
1559
let mainWindow: BrowserWindow | null = null;
1660

1761
export function getMainWindow(): BrowserWindow | null {
@@ -42,11 +86,28 @@ function setupExternalLinkHandlers(window: BrowserWindow): void {
4286

4387
export function createWindow(): void {
4488
const isDev = !app.isPackaged;
89+
const savedState = getSavedWindowState();
90+
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
91+
92+
const scheduleSaveWindowState = (window: BrowserWindow): void => {
93+
if (saveTimeout) {
94+
clearTimeout(saveTimeout);
95+
}
96+
97+
saveTimeout = setTimeout(() => {
98+
if (!window.isDestroyed()) {
99+
saveWindowState(window);
100+
}
101+
saveTimeout = null;
102+
}, 200);
103+
};
45104

46105
mainWindow = new BrowserWindow({
47-
width: 900,
48-
height: 600,
49-
minWidth: 900,
106+
...(savedState.x !== undefined && { x: savedState.x }),
107+
...(savedState.y !== undefined && { y: savedState.y }),
108+
width: savedState.width,
109+
height: savedState.height,
110+
minWidth: 1200,
50111
minHeight: 600,
51112
backgroundColor: "#0a0a0a",
52113
titleBarStyle: "hiddenInset",
@@ -63,10 +124,25 @@ export function createWindow(): void {
63124
});
64125

65126
mainWindow.once("ready-to-show", () => {
66-
mainWindow?.maximize();
127+
if (savedState.isMaximized) {
128+
mainWindow?.maximize();
129+
}
67130
mainWindow?.show();
68131
});
69132

133+
// Persist window state on changes
134+
mainWindow.on(
135+
"resize",
136+
() => mainWindow && scheduleSaveWindowState(mainWindow),
137+
);
138+
mainWindow.on(
139+
"move",
140+
() => mainWindow && scheduleSaveWindowState(mainWindow),
141+
);
142+
mainWindow.on("maximize", () => mainWindow && saveWindowState(mainWindow));
143+
mainWindow.on("unmaximize", () => mainWindow && saveWindowState(mainWindow));
144+
mainWindow.on("close", () => mainWindow && saveWindowState(mainWindow));
145+
70146
setMainWindowGetter(() => mainWindow);
71147

72148
createIPCHandler({
@@ -86,6 +162,10 @@ export function createWindow(): void {
86162
}
87163

88164
mainWindow.on("closed", () => {
165+
if (saveTimeout) {
166+
clearTimeout(saveTimeout);
167+
saveTimeout = null;
168+
}
89169
mainWindow = null;
90170
});
91171
}

0 commit comments

Comments
 (0)