import { app, screen, type BrowserWindow, type Rectangle } from 'electron' import { join } from 'path' import { readFileSync, writeFileSync } from 'fs' import { logger } from './logger' /** * Persist and restore the main window's size / position / maximized state across * launches (W2 / UX19) — Windows apps are expected to reopen where you left them. * Stored as one small JSON object in `/window-state.json`; a disconnected * monitor is handled by a visibility check so the window can never restore off-screen. */ interface WindowState { bounds: Rectangle maximized: boolean } // Matches the createWindow() defaults; used on first run and when a saved position // is no longer on any connected display. const DEFAULT_SIZE = { width: 920, height: 700 } function stateFile(): string { return join(app.getPath('userData'), 'window-state.json') } function readState(): WindowState | null { try { const s = JSON.parse(readFileSync(stateFile(), 'utf8')) as Partial if (s?.bounds && typeof s.bounds.width === 'number' && typeof s.bounds.height === 'number') { return { bounds: s.bounds as Rectangle, maximized: !!s.maximized } } } catch { /* first run or corrupt — fall back to defaults */ } return null } /** True when `b` overlaps the work area of some connected display (i.e. is reachable). */ function isOnSomeDisplay(b: Rectangle): boolean { return screen.getAllDisplays().some((d) => { const wa = d.workArea return ( b.x < wa.x + wa.width && b.x + b.width > wa.x && b.y < wa.y + wa.height && b.y + b.height > wa.y ) }) } /** * The bounds + maximized flag to open the window with. Returns just a size (Electron * then centers it) when there's no usable saved position — first run, or the saved * monitor is gone. */ export function initialWindowState(): { width: number height: number x?: number y?: number maximized: boolean } { const saved = readState() if (saved && isOnSomeDisplay(saved.bounds)) { return { ...saved.bounds, maximized: saved.maximized } } return { ...DEFAULT_SIZE, maximized: saved?.maximized ?? false } } /** * Persist the window's current *normal* bounds (un-maximized) + maximized flag. * Best-effort; a write failure is logged, never thrown. Callers should debounce. */ export function saveWindowState(win: BrowserWindow): void { if (win.isDestroyed() || win.isMinimized()) return try { const state: WindowState = { bounds: win.getNormalBounds(), maximized: win.isMaximized() } writeFileSync(stateFile(), JSON.stringify(state)) } catch (e) { logger.warn('failed to save window state', e) } }