import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { app } from 'electron/main' export interface DesktopAppSettings { openAtLogin: boolean keepRunningInBackground: boolean } export type DesktopAppSettingKey = keyof DesktopAppSettings interface DesktopAppInternalFlags { hasShownBackgroundBalloon: boolean } const DEFAULT_DESKTOP_APP_SETTINGS: DesktopAppSettings = { openAtLogin: false, keepRunningInBackground: true, } const DEFAULT_INTERNAL_FLAGS: DesktopAppInternalFlags = { hasShownBackgroundBalloon: false, } let cachedSettings: DesktopAppSettings = { ...DEFAULT_DESKTOP_APP_SETTINGS } let cachedFlags: DesktopAppInternalFlags = { ...DEFAULT_INTERNAL_FLAGS } function settingsPath(): string { return join(app.getPath('userData'), 'desktop-settings.json') } function normalizePersistedFile(input: unknown): { settings: DesktopAppSettings flags: DesktopAppInternalFlags } { if (!input || typeof input !== 'object') { return { settings: { ...DEFAULT_DESKTOP_APP_SETTINGS }, flags: { ...DEFAULT_INTERNAL_FLAGS }, } } const candidate = input as Partial< Record > return { settings: { openAtLogin: typeof candidate.openAtLogin === 'boolean' ? candidate.openAtLogin : DEFAULT_DESKTOP_APP_SETTINGS.openAtLogin, keepRunningInBackground: typeof candidate.keepRunningInBackground === 'boolean' ? candidate.keepRunningInBackground : DEFAULT_DESKTOP_APP_SETTINGS.keepRunningInBackground, }, flags: { hasShownBackgroundBalloon: typeof candidate.hasShownBackgroundBalloon === 'boolean' ? candidate.hasShownBackgroundBalloon : DEFAULT_INTERNAL_FLAGS.hasShownBackgroundBalloon, }, } } function readPersistedFile(): { settings: DesktopAppSettings flags: DesktopAppInternalFlags } { try { return normalizePersistedFile(JSON.parse(readFileSync(settingsPath(), 'utf8'))) } catch { return { settings: { ...DEFAULT_DESKTOP_APP_SETTINGS }, flags: { ...DEFAULT_INTERNAL_FLAGS }, } } } function persistFile(): void { try { writeFileSync( settingsPath(), JSON.stringify({ ...cachedSettings, ...cachedFlags }), 'utf8', ) } catch (error) { console.warn('[desktop-settings] persist failed', error) } } function readSystemOpenAtLogin(fallback: boolean): boolean { try { return app.getLoginItemSettings().openAtLogin } catch (error) { console.warn('[desktop-settings] read login item failed', error) return fallback } } // Platform-specific login-item application. // // macOS — `setLoginItemSettings({ openAtLogin })` registers a LaunchAgent via // the ServiceManagement framework. `path`/`args` are ignored on darwin; the // system always launches the bundled .app. We keep `openAsHidden: false` // because the user wants the main window to appear at login. // // Windows — `setLoginItemSettings` writes to // `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`. In dev the registry // would point at electron.exe, so we explicitly pass `path: process.execPath` // and an empty `args: []` so the production NSIS-installed exe is registered // correctly. No `--hidden` flag because the user wants a normal window at // startup. // // Linux — `setLoginItemSettings` is a no-op in Electron; we silently accept // the value but the OS will not honor it. function applyOpenAtLogin(openAtLogin: boolean): boolean { try { if (process.platform === 'darwin') { app.setLoginItemSettings({ openAtLogin, openAsHidden: false, }) } else if (process.platform === 'win32') { app.setLoginItemSettings({ openAtLogin, path: process.execPath, args: [], }) } else { app.setLoginItemSettings({ openAtLogin }) } return readSystemOpenAtLogin(openAtLogin) } catch (error) { console.warn('[desktop-settings] apply login item failed', error) return openAtLogin } } export function initDesktopAppSettings(): DesktopAppSettings { const { settings, flags } = readPersistedFile() cachedSettings = { ...settings, openAtLogin: applyOpenAtLogin(settings.openAtLogin), } cachedFlags = flags persistFile() return getDesktopAppSettings() } export function getDesktopAppSettings(): DesktopAppSettings { cachedSettings = { ...cachedSettings, openAtLogin: readSystemOpenAtLogin(cachedSettings.openAtLogin), } return { ...cachedSettings } } export function setDesktopAppSetting( key: DesktopAppSettingKey, value: boolean, ): DesktopAppSettings { const next: DesktopAppSettings = { ...cachedSettings, [key]: value, } if (key === 'openAtLogin') { next.openAtLogin = applyOpenAtLogin(value) } cachedSettings = next persistFile() return getDesktopAppSettings() } export function hasShownBackgroundBalloon(): boolean { return cachedFlags.hasShownBackgroundBalloon } export function markBackgroundBalloonShown(): void { if (cachedFlags.hasShownBackgroundBalloon) { return } cachedFlags = { ...cachedFlags, hasShownBackgroundBalloon: true } persistFile() }