diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 7c947db..df8d798 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -108,7 +108,9 @@ const handlers = { "debug:repoFlush": (name) => guard(async () => debug.repoFlush(name)), "debug:openWindow": () => guard(async () => openDebugWindow()), } satisfies { - [C in IpcRequestChannel]: (...args: Parameters) => Promise>; + [C in IpcRequestChannel]: ( + ...args: Parameters + ) => Promise>; }; export function registerIpcHandlers(): void { @@ -121,7 +123,5 @@ function register(channel: C): void { const handler = handlers[channel] as ( ...args: Parameters ) => Promise>; - ipcMain.handle(channel, (_event, ...args) => - handler(...(args as Parameters)), - ); + ipcMain.handle(channel, (_event, ...args) => handler(...(args as Parameters))); } diff --git a/src/main/windowState.ts b/src/main/windowState.ts new file mode 100644 index 0000000..d71ccb5 --- /dev/null +++ b/src/main/windowState.ts @@ -0,0 +1,65 @@ +import { app, screen, type BrowserWindow, type Rectangle } from "electron"; +import { join } from "node:path"; +import { readFileSync } from "node:fs"; +import { writeFileAtomicSync } from "./lib/atomicFile"; + +type Bounds = Rectangle & { maximized: boolean }; + +const path = () => join(app.getPath("userData"), "window-state.json"); + +function read(): Partial | null { + try { + return JSON.parse(readFileSync(path(), "utf8")) as Partial; + } catch { + return null; + } +} + +function onScreen(b: Rectangle): boolean { + return screen.getAllDisplays().some((d) => { + const w = d.workArea; + return ( + b.x < w.x + w.width && b.x + b.width > w.x && b.y < w.y + w.height && b.y + b.height > w.y + ); + }); +} + +export function restoreBounds(): { bounds?: Rectangle; maximized: boolean } { + const saved = read(); + if ( + saved && + saved.width && + saved.height && + saved.x != null && + saved.y != null && + onScreen({ x: saved.x, y: saved.y, width: saved.width, height: saved.height }) + ) { + return { + bounds: { x: saved.x, y: saved.y, width: saved.width, height: saved.height }, + maximized: saved.maximized ?? false, + }; + } + return { maximized: saved?.maximized ?? false }; +} + +export function trackBounds(win: BrowserWindow): void { + let normalBounds = win.getBounds(); + + const save = (): void => { + if (win.isDestroyed()) return; + writeFileAtomicSync( + path(), + JSON.stringify({ ...normalBounds, maximized: win.isMaximized() }, null, 2), + ); + }; + + const remember = (): void => { + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { + normalBounds = win.getBounds(); + } + }; + + win.on("resize", remember); + win.on("move", remember); + win.on("close", save); +} diff --git a/src/main/windows.ts b/src/main/windows.ts index 5ac6d49..a2d2b28 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -3,6 +3,7 @@ import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import type { IpcEventChannel, IpcEvents } from "../shared/ipc"; import { TRAFFIC_LIGHT_INSET } from "../shared/window"; +import { restoreBounds, trackBounds } from "./windowState"; const __dirname = dirname(fileURLToPath(import.meta.url)); const isDev = !app.isPackaged; @@ -25,9 +26,11 @@ function load(win: BrowserWindow, hash = ""): void { } export function createMainWindow(): BrowserWindow { + const restored = restoreBounds(); mainWindow = new BrowserWindow({ width: 1180, height: 760, + ...restored.bounds, minWidth: 940, minHeight: 600, show: false, @@ -43,6 +46,9 @@ export function createMainWindow(): BrowserWindow { }, }); + if (restored.maximized) mainWindow.maximize(); + trackBounds(mainWindow); + mainWindow.once("ready-to-show", () => mainWindow?.show()); mainWindow.webContents.setWindowOpenHandler(({ url }) => { void shell.openExternal(url);