// SPDX-License-Identifier: AGPL-3.0-or-later import fs from 'node:fs'; import path from 'node:path'; import { DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, DESKTOP_FIRST_CONTENT_PAINTED_CHANNEL, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, } from '@electron/common/Constants'; import {getDesktopWindowBehaviorSettings, getPrebootTheme} from '@electron/common/DesktopConfig'; import {createChildLogger} from '@electron/common/Logger'; import type {DesktopWindowBehaviorSettings} from '@electron/common/Types'; import {createAppLoadRetry} from '@electron/main/AppLoadRetry'; import { shouldForwardRendererConsoleToMainLog, shouldIgnoreWindowStateForLaunch, shouldOpenDevToolsOnLaunch, } from '@electron/main/DesktopDebugInfo'; import {getDesktopDistributionPath} from '@electron/main/DesktopDistributionPath'; import {resolveDesktopLandingUrl} from '@electron/main/DesktopLastRoute'; import {DesktopRuntimeSecurity} from '@electron/main/DesktopRuntimeSecurity'; import {hasActiveDesktopTray, refreshDesktopTrayMenu} from '@electron/main/DesktopTray'; import {drainPendingDisplayMediaRequests, registerDisplayMediaRequestHandler} from '@electron/main/DisplayMedia'; import {isNativeGatewayAvailable} from '@electron/main/GatewaySocketNativeBoundary'; import {shouldDisableV8CodeCache} from '@electron/main/LaunchOptions'; import {getDesktopLocalAppAuthorization} from '@electron/main/LocalAppProtocolAuthorization'; import {isLocalAppRendererDocumentURL, isLocalAppURL} from '@electron/main/LocalAppURL'; import {t} from '@electron/main/MainI18n'; import {MainWindowRevealGate, MainWindowRevealReason} from '@electron/main/MainWindowReveal'; import {signalMainWindowCreated, signalMainWindowReady} from '@electron/main/ModuleBootHandoff'; import {openExternalDeduped} from '@electron/main/OpenExternal'; import {registerSpellcheck} from '@electron/main/Spellcheck'; import {resetStreamingPriority} from '@electron/main/StreamingPriority'; import {getMainWindowRendererGoneAction} from '@electron/main/WindowRendererLifecycle'; import {refreshWindowsBadgeOverlay} from '@electron/main/WindowsBadge'; import {NATIVE_GATEWAY_TRANSPORT_AVAILABLE_RENDERER_ARG} from '@fluxer/desktop_ipc/src/GatewayTransportContract'; import {app, BrowserWindow, dialog, ipcMain, screen} from 'electron'; import log from 'electron-log'; const logger = createChildLogger('Window'); const runtimeSecurity = new DesktopRuntimeSecurity({ logger: createChildLogger('DesktopRuntimeSecurity'), localAppAuthorization: getDesktopLocalAppAuthorization(), }); const LIVE_RESIZE_IDLE_MS = 400; const VISIBILITY_MARGIN = 32; const RENDERER_GONE_REPEAT_WINDOW_MS = 30000; const CLOSE_FOR_UPDATE_TIMEOUT_MS = 5000; const THEME_WINDOW_BACKGROUND_COLORS: Readonly> = Object.freeze({ dark: '#1a181e', light: '#ebecef', coal: '#020203', dark_legacy: '#191b20', }); const DEFAULT_WINDOW_BACKGROUND_COLOR = '#1a181e'; const TRANSPARENT_WINDOW_BACKGROUND_COLOR = '#00000000'; const MEDIA_DEVICE_BLINK_FEATURES = 'EnumerateDevices,AudioOutputDevices'; const ACTIVE_ALLOW_TRANSPARENCY_RENDERER_ARG = '--fluxer-active-allow-transparency=1'; const ACTIVE_USE_NATIVE_TITLEBAR_RENDERER_ARG = '--fluxer-active-use-native-titlebar=1'; const THEME_STUDIO_POPOUT_WINDOW_NAME = 'fluxer_theme_studio'; const THEME_STUDIO_POPOUT_PATHNAME = '/theme-studio'; const THEME_STUDIO_POPOUT_TITLE = 'Fluxer | Theme Studio'; const THEME_STUDIO_POPOUT_MIN_WIDTH = 900; const THEME_STUDIO_POPOUT_MIN_HEIGHT = 620; export const THEME_STUDIO_POPOUT_KEY = THEME_STUDIO_POPOUT_WINDOW_NAME; const VOICE_POPOUT_WINDOW_NAME_PREFIX = 'fluxer-voice-popout:'; const VOICE_POPOUT_WINDOW_NAME_LENGTH_MAX = 256; const VOICE_POPOUT_MIN_WIDTH = 360; const VOICE_POPOUT_MIN_HEIGHT = 240; const VOICE_POPOUT_WINDOWS_MAX = 8; const CUSTOM_TITLEBAR_HEIGHT_MAC = 32; const CUSTOM_TITLEBAR_TRAFFIC_LIGHT_DIAMETER = 14; const CUSTOM_TITLEBAR_TRAFFIC_LIGHT_POSITION = { x: 12, y: Math.round((CUSTOM_TITLEBAR_HEIGHT_MAC - CUSTOM_TITLEBAR_TRAFFIC_LIGHT_DIAMETER) / 2), }; const webAuthnDeviceTypes = new Set(['hid', 'usb', 'serial', 'bluetooth']); const webAuthnPermissionTypes = new Set(['hid', 'usb', 'serial', 'bluetooth']); const trustedRendererPermissionTypes = new Set([ 'media', 'display-capture', 'notifications', 'fullscreen', 'pointerLock', 'speaker-selection', 'clipboard-sanitized-write', ]); const POPOUT_NAMESPACE = 'fluxer_'; export function isTrustedOrigin(url?: string): boolean { return isLocalAppRendererDocumentURL(url ?? null); } function getSanitizedPath(rawUrl: string): string | null { try { return new URL(rawUrl).pathname; } catch (error) { log.warn('Invalid URL for path check', {rawUrl, error}); return null; } } function preventUntrustedNavigation(event: Electron.Event, url: string, isMainFrame: boolean): void { if (isLocalAppURL(url) && !isLocalAppRendererDocumentURL(url)) { logger.warn('Blocked a navigation to a reserved local app path', {url, isMainFrame}); event.preventDefault(); return; } if (isMainFrame && !isTrustedOrigin(url)) { event.preventDefault(); } } function preventUntrustedChildNavigation(event: Electron.Event, url: string, isMainFrame: boolean): void { if (isMainFrame && url === 'about:blank') return; preventUntrustedNavigation(event, url, isMainFrame); } function attachChildWindowGuards(webContents: Electron.WebContents): void { getDesktopLocalAppAuthorization().authorize(webContents); webContents.on('will-navigate', (event, url) => { preventUntrustedChildNavigation(event, url, true); }); webContents.on('will-frame-navigate', (event) => { preventUntrustedChildNavigation(event, event.url, event.isMainFrame); }); webContents.setWindowOpenHandler(({url}) => { openExternalDeduped(url).catch((error) => { log.warn('Failed to open external URL from a popout window-open:', error); }); return {action: 'deny'}; }); } interface WindowBounds { x: number; y: number; width: number; height: number; isMaximized: boolean; } interface CreateWindowOptions { startHidden?: boolean; } let mainWindow: BrowserWindow | null = null; let windowStateFile: string; let isQuitting = false; let initialUseNativeTitleBar: boolean | null = null; let initialAllowTransparency: boolean | null = null; let themeStudioPopoutWindow: BrowserWindow | null = null; let lastRestorableMainWindowMaximized = false; let mainWindowRendererGone = false; let closingMainWindowForUpdate = false; let mainWindowTakeover: (() => void) | null = null; const takeoverEndedListeners = new Set<() => void>(); let pendingMainWindowReveal: {readonly window: BrowserWindow; readonly requestShow: () => void} | null = null; const maximizeChangeForwarders = new WeakSet(); const voicePopoutWindows = new Map(); const windowsHtmlFullscreenStates = new WeakMap< BrowserWindow, {resizable: boolean; bounds: Bounds; isMaximized: boolean; customChromeGuardActive: boolean} >(); let lastGoodWindowBounds: Bounds | null = null; function getWindowStateFile(): string { if (!windowStateFile) { const userDataPath = app.getPath('userData'); windowStateFile = path.join(userDataPath, 'window-state.json'); } return windowStateFile; } interface Bounds { x: number; y: number; width: number; height: number; } function boundsIntersect(a: Bounds, b: Bounds): boolean { const aRight = a.x + a.width; const bRight = b.x + b.width; const aBottom = a.y + a.height; const bBottom = b.y + b.height; const overlapX = Math.min(aRight, bRight) - Math.max(a.x, b.x); const overlapY = Math.min(aBottom, bBottom) - Math.max(a.y, b.y); return overlapX > 0 && overlapY > 0; } function findVisibleDisplay(displays: Array, bounds: Bounds): Electron.Display | undefined { return displays.find((display) => { const visibleArea = { x: display.workArea.x + VISIBILITY_MARGIN, y: display.workArea.y + VISIBILITY_MARGIN, width: display.workArea.width - 2 * VISIBILITY_MARGIN, height: display.workArea.height - 2 * VISIBILITY_MARGIN, }; return boundsIntersect(bounds, visibleArea); }); } function getDefaultBoundsForDisplay(display: Electron.Display): Bounds { const {workArea} = display; const width = Math.min(DEFAULT_WINDOW_WIDTH, workArea.width); const height = Math.min(DEFAULT_WINDOW_HEIGHT, workArea.height); return { x: Math.round(workArea.x + (workArea.width - width) / 2), y: Math.round(workArea.y + (workArea.height - height) / 2), width, height, }; } function savedBoundsExceedWorkArea(bounds: Bounds, display: Electron.Display): boolean { const {workArea} = display; return ( bounds.x < workArea.x || bounds.y < workArea.y || bounds.x + bounds.width > workArea.x + workArea.width || bounds.y + bounds.height > workArea.y + workArea.height ); } function sanitizeLoadedWindowBounds(bounds: WindowBounds, display: Electron.Display): WindowBounds { if (!bounds.isMaximized || !savedBoundsExceedWorkArea(bounds, display)) { return bounds; } const defaultBounds = getDefaultBoundsForDisplay(display); const sanitizedBounds = { ...defaultBounds, isMaximized: true, }; log.info('Ignoring display-sized normal bounds from saved maximized window state:', { savedBounds: bounds, sanitizedBounds, }); return sanitizedBounds; } function ensureWindowOnScreen(window: BrowserWindow): void { const bounds = window.getBounds(); const displays = screen.getAllDisplays(); const visibleDisplay = findVisibleDisplay(displays, bounds); if (!visibleDisplay && displays.length > 0) { const primaryBounds = displays[0].bounds; const correctedBounds = { x: primaryBounds.x, y: primaryBounds.y, width: Math.min(bounds.width, primaryBounds.width), height: Math.min(bounds.height, primaryBounds.height), }; log.warn('Window is off-screen, repositioning to primary display:', correctedBounds); window.setBounds(correctedBounds); } } function loadWindowBounds(): Partial | null { if (shouldIgnoreWindowStateForLaunch(process.argv)) { log.info('Ignoring saved window bounds for this launch'); return null; } if (!getDesktopWindowBehaviorSettings().rememberWindowState) { return null; } try { const filePath = getWindowStateFile(); if (fs.existsSync(filePath)) { const data = fs.readFileSync(filePath, 'utf-8'); const bounds = JSON.parse(data) as WindowBounds; const displays = screen.getAllDisplays(); const display = findVisibleDisplay(displays, bounds); if (display != null) { const sanitizedBounds = sanitizeLoadedWindowBounds(bounds, display); log.info('Restored window bounds:', sanitizedBounds); return sanitizedBounds; } else { log.warn('Saved window position is off-screen, using defaults'); } } } catch (error) { log.error('Failed to load window bounds:', error); } return null; } function saveWindowBounds(): void { if (!mainWindow) return; if (!getDesktopWindowBehaviorSettings().rememberWindowState) return; if (windowsHtmlFullscreenStates.has(mainWindow)) return; try { const bounds = mainWindow.getNormalBounds(); const isMaximized = mainWindow.isMinimized() ? lastRestorableMainWindowMaximized : mainWindow.isMaximized(); lastRestorableMainWindowMaximized = isMaximized; const windowState: WindowBounds = { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, isMaximized, }; lastGoodWindowBounds = bounds; const filePath = getWindowStateFile(); fs.writeFileSync(filePath, JSON.stringify(windowState, null, 2), 'utf-8'); log.debug('Saved window bounds:', windowState); } catch (error) { log.error('Failed to save window bounds:', error); } } export function clearSavedWindowBounds(): void { try { const filePath = getWindowStateFile(); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); log.info('Cleared saved window bounds'); } } catch (error) { log.error('Failed to clear saved window bounds:', error); } } function shouldHideMainWindowOnClose(): boolean { if (process.platform === 'darwin') { return true; } const settings = getDesktopWindowBehaviorSettings(); return hasActiveDesktopTray() && settings.showTrayIcon && settings.closeToTray; } function shouldHideMainWindowOnMinimize(): boolean { const settings = getDesktopWindowBehaviorSettings(); return hasActiveDesktopTray() && settings.showTrayIcon && settings.minimizeToTray; } export function getMainWindow(): BrowserWindow | null { return mainWindow; } export function getActiveUseNativeTitleBar(): boolean { return initialUseNativeTitleBar ?? false; } export function getActiveAllowTransparency(): boolean { return initialAllowTransparency ?? false; } function isAliveWindow(window: BrowserWindow | null): window is BrowserWindow { return Boolean(window && !window.isDestroyed()); } function reloadMainWindowRendererAfterGone(reason: string, details?: Electron.RenderProcessGoneDetails): void { if (!isAliveWindow(mainWindow)) return; if (mainWindow.webContents.isDestroyed()) return; mainWindowRendererGone = false; logger.warn('Reloading main window renderer after termination', {reason, details}); mainWindow.webContents.reloadIgnoringCache(); } function recoverMainWindowRendererBeforeShow(reason: string): void { if (!mainWindowRendererGone) return; if (!isAliveWindow(mainWindow)) return; if (mainWindow.webContents.isDestroyed() || mainWindow.webContents.isLoadingMainFrame()) return; reloadMainWindowRendererAfterGone(reason); } export function isAppDocumentWindowContents(contents: Electron.WebContents): boolean { return [mainWindow, themeStudioPopoutWindow].some( (window) => isAliveWindow(window) && window.webContents === contents, ); } function getThemeStudioPopoutWindow(): BrowserWindow | null { if (isAliveWindow(themeStudioPopoutWindow)) { return themeStudioPopoutWindow; } themeStudioPopoutWindow = null; for (const window of BrowserWindow.getAllWindows()) { if (window === mainWindow || window.isDestroyed()) continue; if (getSanitizedPath(window.webContents.getURL()) === THEME_STUDIO_POPOUT_PATHNAME) { trackThemeStudioPopoutWindow(window); return window; } } return null; } export function focusWindow(window: BrowserWindow): void { if (window.isMinimized()) { window.restore(); } if (!window.isVisible()) { window.show(); } try { window.moveTop(); } catch (error) { logger.warn('Failed to move window to top before focusing', error); } window.focus(); } export function focusThemeStudioPopoutWindow(): boolean { const window = getThemeStudioPopoutWindow(); if (!window) { return false; } focusWindow(window); return true; } export function closeThemeStudioPopoutWindow(): boolean { const window = getThemeStudioPopoutWindow(); if (!window) { return false; } window.close(); return true; } function isVoicePopoutWindowName(frameName: string | undefined): frameName is string { if (typeof frameName !== 'string') return false; if (!frameName.startsWith(VOICE_POPOUT_WINDOW_NAME_PREFIX)) return false; return frameName.length <= VOICE_POPOUT_WINDOW_NAME_LENGTH_MAX; } function pruneDestroyedVoicePopoutWindows(): void { for (const [key, window] of voicePopoutWindows) { if (!isAliveWindow(window)) { voicePopoutWindows.delete(key); } } } function hasVoicePopoutCapacity(): boolean { pruneDestroyedVoicePopoutWindows(); return voicePopoutWindows.size < VOICE_POPOUT_WINDOWS_MAX; } function trackVoicePopoutWindow(key: string, window: BrowserWindow): void { const existing = voicePopoutWindows.get(key); if (existing && existing !== window && isAliveWindow(existing)) { existing.close(); } voicePopoutWindows.set(key, window); window.setMinimumSize(VOICE_POPOUT_MIN_WIDTH, VOICE_POPOUT_MIN_HEIGHT); forwardMaximizeChanges(window); window.once('closed', () => { if (voicePopoutWindows.get(key) === window) { voicePopoutWindows.delete(key); } }); } function getVoicePopoutWindow(key: string): BrowserWindow | null { const window = voicePopoutWindows.get(key) ?? null; if (isAliveWindow(window)) { return window; } if (window) { voicePopoutWindows.delete(key); } return null; } export function setVoicePopoutAlwaysOnTop(key: string, flag: boolean): boolean { const window = getVoicePopoutWindow(key); if (!window) { return false; } window.setAlwaysOnTop(flag); return true; } export function setThemeStudioPopoutAlwaysOnTop(flag: boolean): boolean { const window = getThemeStudioPopoutWindow(); if (!window) { return false; } window.setAlwaysOnTop(flag); return true; } export function focusVoicePopoutWindow(key: string): boolean { const window = getVoicePopoutWindow(key); if (!window) { return false; } focusWindow(window); return true; } function getEffectiveUseNativeTitleBar(settings: DesktopWindowBehaviorSettings): boolean { if (process.platform === 'darwin') return false; return settings.useNativeTitleBar && !settings.allowTransparency; } function getWindowBackgroundColor(allowTransparency: boolean): string { if (allowTransparency) { return TRANSPARENT_WINDOW_BACKGROUND_COLOR; } return THEME_WINDOW_BACKGROUND_COLORS[getPrebootTheme() ?? ''] ?? DEFAULT_WINDOW_BACKGROUND_COLOR; } function getWindowShadowOptions( allowTransparency: boolean, ): Pick { if (process.platform !== 'linux' || !allowTransparency) return {}; return {hasShadow: false}; } function enterWindowsHtmlFullscreenChromeGuard(window: BrowserWindow): void { if (process.platform !== 'win32') return; if (windowsHtmlFullscreenStates.has(window)) return; const customChromeGuardActive = !getActiveUseNativeTitleBar(); const bounds = lastGoodWindowBounds ?? window.getNormalBounds(); const isMaximized = window.isMaximized(); windowsHtmlFullscreenStates.set(window, { resizable: window.isResizable(), bounds, isMaximized, customChromeGuardActive, }); if (!customChromeGuardActive) return; window.setBackgroundColor('#000000'); window.setResizable(false); } function restoreWindowsHtmlFullscreenBounds( window: BrowserWindow, previous: {bounds: Bounds; isMaximized: boolean}, ): void { if (!isAliveWindow(window) || windowsHtmlFullscreenStates.has(window)) return; if (previous.isMaximized || window.isMaximized()) return; window.setBounds(previous.bounds); saveWindowBounds(); } function leaveWindowsHtmlFullscreenChromeGuard(window: BrowserWindow): void { if (process.platform !== 'win32') return; const previous = windowsHtmlFullscreenStates.get(window); if (!previous) return; windowsHtmlFullscreenStates.delete(window); if (previous.customChromeGuardActive) { window.setResizable(previous.resizable); window.setBackgroundColor(getWindowBackgroundColor(getActiveAllowTransparency())); } restoreWindowsHtmlFullscreenBounds(window, previous); } function installHtmlFullscreenChromeGuard(window: BrowserWindow): void { window.on('enter-html-full-screen', () => { enterWindowsHtmlFullscreenChromeGuard(window); }); window.on('leave-html-full-screen', () => { leaveWindowsHtmlFullscreenChromeGuard(window); }); window.once('closed', () => { windowsHtmlFullscreenStates.delete(window); }); } function getRendererAdditionalArguments( allowTransparency: boolean, useNativeTitleBar: boolean, allowNativeGateway: boolean, ): Array { const args: Array = []; if (allowTransparency) args.push(ACTIVE_ALLOW_TRANSPARENCY_RENDERER_ARG); if (useNativeTitleBar) args.push(ACTIVE_USE_NATIVE_TITLEBAR_RENDERER_ARG); if (allowNativeGateway && isNativeGatewayAvailable()) args.push(NATIVE_GATEWAY_TRANSPORT_AVAILABLE_RENDERER_ARG); return args; } function getDevToolsOptions(options: {forceDetach?: boolean} = {}): Electron.OpenDevToolsOptions | undefined { return options.forceDetach || getActiveAllowTransparency() ? {mode: 'detach', activate: true} : undefined; } function openWindowDevTools(window: BrowserWindow, options?: {forceDetach?: boolean}): void { window.webContents.openDevTools(getDevToolsOptions(options)); } export function toggleWindowDevTools(window: BrowserWindow): void { if (window.webContents.isDevToolsOpened()) { window.webContents.closeDevTools(); return; } openWindowDevTools(window); } function forwardMaximizeChanges(window: BrowserWindow): void { if (maximizeChangeForwarders.has(window)) { return; } maximizeChangeForwarders.add(window); window.on('maximize', () => { window.webContents.send('window-maximize-change', true); }); window.on('unmaximize', () => { window.webContents.send('window-maximize-change', false); }); } function trackThemeStudioPopoutWindow(window: BrowserWindow): void { themeStudioPopoutWindow = window; window.setAlwaysOnTop(true); window.setTitle(THEME_STUDIO_POPOUT_TITLE); window.setMinimumSize(THEME_STUDIO_POPOUT_MIN_WIDTH, THEME_STUDIO_POPOUT_MIN_HEIGHT); forwardMaximizeChanges(window); window.once('closed', () => { if (themeStudioPopoutWindow === window) { themeStudioPopoutWindow = null; } }); } export function desktopUseNativeTitleBarPendingRestart(): boolean { if (process.platform === 'darwin') return false; if (initialUseNativeTitleBar === null) return false; return getEffectiveUseNativeTitleBar(getDesktopWindowBehaviorSettings()) !== initialUseNativeTitleBar; } export function desktopTransparencyPendingRestart(): boolean { if (initialAllowTransparency === null) return false; return getDesktopWindowBehaviorSettings().allowTransparency !== initialAllowTransparency; } function getLinuxWindowIconPath(): string | null { const baseIconName = '512x512.png'; const candidatePaths = [ path.join(process.resourcesPath, 'icons', baseIconName), path.join(process.resourcesPath, baseIconName), path.join(path.dirname(app.getPath('exe')), baseIconName), ]; for (const candidatePath of candidatePaths) { if (fs.existsSync(candidatePath)) { return candidatePath; } } return null; } function getVoicePopoutWindowOptions(): Electron.BrowserWindowConstructorOptions { const isMac = process.platform === 'darwin'; const isLinux = process.platform === 'linux'; const options: Electron.BrowserWindowConstructorOptions = { ...getTitleBarWindowOptions(getActiveUseNativeTitleBar()), minWidth: VOICE_POPOUT_MIN_WIDTH, minHeight: VOICE_POPOUT_MIN_HEIGHT, ...(isMac ? {trafficLightPosition: CUSTOM_TITLEBAR_TRAFFIC_LIGHT_POSITION} : {}), backgroundColor: getWindowBackgroundColor(false), transparent: false, ...getWindowShadowOptions(false), autoHideMenuBar: true, show: true, }; if (isLinux) { const iconPath = getLinuxWindowIconPath(); if (iconPath) { options.icon = iconPath; } } return options; } function getTitleBarWindowOptions( useNativeTitleBar: boolean, ): Pick { const isMac = process.platform === 'darwin'; const isWindows = process.platform === 'win32'; const useCustomChrome = isMac || !useNativeTitleBar; return { titleBarStyle: useCustomChrome ? 'hidden' : undefined, titleBarOverlay: isWindows && useCustomChrome ? false : undefined, frame: isMac ? true : !useCustomChrome, }; } function getSharedWebPreferences( allowTransparency: boolean, useNativeTitleBar: boolean, allowNativeGateway = false, ): Electron.WebPreferences { return { preload: getDesktopDistributionPath('preload', 'index.cjs'), enableBlinkFeatures: MEDIA_DEVICE_BLINK_FEATURES, contextIsolation: true, nodeIntegration: false, sandbox: false, webSecurity: true, allowRunningInsecureContent: false, spellcheck: process.platform !== 'linux', transparent: allowTransparency, additionalArguments: getRendererAdditionalArguments(allowTransparency, useNativeTitleBar, allowNativeGateway), v8CacheOptions: shouldDisableV8CodeCache(process.argv) ? 'none' : 'code', }; } export function createWindow(options: CreateWindowOptions = {}): BrowserWindow { const startedAt = Date.now(); const logPhase = (phase: string): void => { logger.info('Create window phase completed', {phase, elapsedMs: Date.now() - startedAt}); }; const primaryDisplay = screen.getPrimaryDisplay(); const {width: screenWidth, height: screenHeight} = primaryDisplay.workAreaSize; const savedBounds = loadWindowBounds(); lastRestorableMainWindowMaximized = Boolean(savedBounds?.isMaximized); logPhase('bounds'); const windowWidth = savedBounds?.width ?? Math.min(DEFAULT_WINDOW_WIDTH, screenWidth); const windowHeight = savedBounds?.height ?? Math.min(DEFAULT_WINDOW_HEIGHT, screenHeight); const isMac = process.platform === 'darwin'; const isLinux = process.platform === 'linux'; const desktopWindowBehavior = getDesktopWindowBehaviorSettings(); const allowTransparency = desktopWindowBehavior.allowTransparency; const useNativeTitleBar = getEffectiveUseNativeTitleBar(desktopWindowBehavior); const acceptFirstMouseOnFocus = isMac; initialUseNativeTitleBar = useNativeTitleBar; initialAllowTransparency = allowTransparency; const windowOptions: Electron.BrowserWindowConstructorOptions = { width: windowWidth, height: windowHeight, minWidth: MIN_WINDOW_WIDTH, minHeight: MIN_WINDOW_HEIGHT, show: false, backgroundColor: getWindowBackgroundColor(allowTransparency), transparent: allowTransparency, ...getWindowShadowOptions(allowTransparency), ...getTitleBarWindowOptions(useNativeTitleBar), ...(isMac ? {trafficLightPosition: CUSTOM_TITLEBAR_TRAFFIC_LIGHT_POSITION} : {}), acceptFirstMouse: acceptFirstMouseOnFocus, webPreferences: getSharedWebPreferences(allowTransparency, useNativeTitleBar, true), }; if (isLinux) { const iconPath = getLinuxWindowIconPath(); if (iconPath) { windowOptions.icon = iconPath; } } if (savedBounds?.x !== undefined && savedBounds?.y !== undefined) { windowOptions.x = savedBounds.x; windowOptions.y = savedBounds.y; } else { windowOptions.center = true; } mainWindow = new BrowserWindow(windowOptions); signalMainWindowCreated(mainWindow); mainWindowRendererGone = false; installHtmlFullscreenChromeGuard(mainWindow); lastGoodWindowBounds = mainWindow.getNormalBounds(); logPhase('browser-window'); let pendingMaximize = Boolean(savedBounds?.isMaximized); const applyPendingMaximize = (window: BrowserWindow): void => { if (!pendingMaximize) return; pendingMaximize = false; window.maximize(); }; const createdWindow = mainWindow; createdWindow.once('show', () => applyPendingMaximize(createdWindow)); let revealShowsWindow = !options.startHidden; const releasePendingReveal = (): void => { if (pendingMainWindowReveal?.window === createdWindow) { pendingMainWindowReveal = null; } }; const revealGate = new MainWindowRevealGate({ onReveal: (reason) => { releasePendingReveal(); logger.info('Main window revealed', {reason, revealShowsWindow, elapsedMs: Date.now() - startedAt}); if (reason !== MainWindowRevealReason.CONTENT_PAINTED) { logger.warn('The renderer never reported painted content, revealing the main window anyway', {reason}); } if (!revealShowsWindow) { signalMainWindowReady(); return; } if (!isAliveWindow(createdWindow) || createdWindow.isVisible()) return; applyPendingMaximize(createdWindow); createdWindow.show(); }, }); pendingMainWindowReveal = { window: createdWindow, requestShow: () => { revealShowsWindow = true; }, }; const onFirstContentPainted = (event: Electron.IpcMainEvent): void => { if (createdWindow.isDestroyed() || event.sender !== createdWindow.webContents) return; if (event.senderFrame == null || event.senderFrame.parent != null) return; revealGate.markContentPainted(); }; ipcMain.on(DESKTOP_FIRST_CONTENT_PAINTED_CHANNEL, onFirstContentPainted); createdWindow.once('closed', () => { ipcMain.removeListener(DESKTOP_FIRST_CONTENT_PAINTED_CHANNEL, onFirstContentPainted); releasePendingReveal(); revealGate.dispose(); }); createdWindow.once('show', () => { releasePendingReveal(); revealGate.dispose(); signalMainWindowReady(); }); createdWindow.once('ready-to-show', () => revealGate.markReadyToShow()); revealGate.start(); let saveTimeout: NodeJS.Timeout | null = null; const debouncedSave = () => { if (saveTimeout) clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveWindowBounds(); }, 500); }; mainWindow.on('resize', debouncedSave); mainWindow.on('move', debouncedSave); let liveResizeActive = false; let liveResizeIdleTimeout: NodeJS.Timeout | null = null; const sendLiveResizeState = (active: boolean) => { if (liveResizeActive === active) return; liveResizeActive = active; if (!mainWindow || mainWindow.webContents.isDestroyed()) return; mainWindow.webContents.send('window-live-resize-change', active); }; const endLiveResize = () => { if (liveResizeIdleTimeout) { clearTimeout(liveResizeIdleTimeout); liveResizeIdleTimeout = null; } sendLiveResizeState(false); }; mainWindow.on('will-resize', () => { sendLiveResizeState(true); if (liveResizeIdleTimeout) clearTimeout(liveResizeIdleTimeout); liveResizeIdleTimeout = setTimeout(endLiveResize, LIVE_RESIZE_IDLE_MS); }); mainWindow.on('resized', endLiveResize); mainWindow.on('blur', endLiveResize); mainWindow.on('maximize', () => { lastRestorableMainWindowMaximized = true; saveWindowBounds(); mainWindow?.webContents.send('window-maximize-change', true); }); mainWindow.on('unmaximize', () => { const window = mainWindow; setTimeout(() => { if (mainWindow !== window) return; if (!isAliveWindow(window)) return; if (!window.isMinimized()) { lastRestorableMainWindowMaximized = false; } saveWindowBounds(); }, 0); mainWindow?.webContents.send('window-maximize-change', false); }); mainWindow.on('minimize', () => { if (!isQuitting && shouldHideMainWindowOnMinimize()) { mainWindow?.hide(); refreshDesktopTrayMenu(); } }); mainWindow.on('restore', refreshDesktopTrayMenu); mainWindow.on('show', refreshDesktopTrayMenu); mainWindow.on('hide', refreshDesktopTrayMenu); mainWindow.on('close', (event) => { if (saveTimeout) clearTimeout(saveTimeout); endLiveResize(); saveWindowBounds(); if (!isQuitting && !closingMainWindowForUpdate && shouldHideMainWindowOnClose()) { event.preventDefault(); logger.info( process.platform === 'darwin' ? 'Window close hid the app. Use Quit to terminate the process' : 'Window close hid the app to the tray. Use Quit to terminate the process', ); mainWindow?.hide(); refreshDesktopTrayMenu(); } }); mainWindow.on('closed', () => { mainWindow = null; }); mainWindow.setMenuBarVisibility(false); if (process.platform === 'win32') { mainWindow.on('show', () => { refreshWindowsBadgeOverlay(mainWindow); }); } const webContents = mainWindow.webContents; const session = webContents.session; runtimeSecurity.install(session); let rendererGoneReloaded = false; let lastRendererGoneAt = 0; if (shouldOpenDevToolsOnLaunch(process.argv)) { logger.info('Opening DevTools for this launch'); setTimeout(() => { if (isAliveWindow(mainWindow)) { openWindowDevTools(mainWindow, {forceDetach: true}); } }, 0); } if (shouldForwardRendererConsoleToMainLog(process.argv)) { webContents.on('console-message', (event, _legacyLevel, legacyMessage, legacyLine, legacySourceId) => { const details = event as Electron.Event; const level = details.level ?? 'info'; const message = details.message || legacyMessage; const metadata = { level, sourceId: details.sourceId || legacySourceId || undefined, lineNumber: details.lineNumber || legacyLine || undefined, }; if (level === 'error') { logger.error('Renderer console:', message, metadata); } else if (level === 'warning') { logger.warn('Renderer console:', message, metadata); } else if (level === 'debug') { logger.debug('Renderer console:', message, metadata); } else { logger.info('Renderer console:', message, metadata); } }); } webContents.on('preload-error', (_event, preloadPath, error) => { logger.error('Preload script failed:', {preloadPath, error}); }); webContents.on('unresponsive', () => { logger.warn('Renderer became unresponsive', {url: webContents.getURL()}); }); webContents.on('responsive', () => { logger.info('Renderer became responsive', {url: webContents.getURL()}); }); webContents.on('render-process-gone', (_event, details) => { logger.error('Render process gone', {url: webContents.getURL(), details}); drainPendingDisplayMediaRequests(`render-process-gone:${details.reason}`); resetStreamingPriority(); const now = Date.now(); const repeated = now - lastRendererGoneAt < RENDERER_GONE_REPEAT_WINDOW_MS; lastRendererGoneAt = now; mainWindowRendererGone = true; const action = getMainWindowRendererGoneAction(details, { platform: process.platform, isQuitting, isMainWindowHidden: isAliveWindow(mainWindow) && !mainWindow.isVisible(), closeToTrayEnabled: shouldHideMainWindowOnClose(), reloadedRecently: rendererGoneReloaded && repeated, }); if (action === 'quit') { logger.error('Quitting after renderer process termination', {details, repeated}); app.quit(); return; } if (action === 'defer-reload') { logger.warn('Deferring hidden main window renderer recovery until the window is shown', {details, repeated}); return; } if (action === 'reload' && isAliveWindow(mainWindow)) { rendererGoneReloaded = true; reloadMainWindowRendererAfterGone(`render-process-gone:${details.reason}`, details); } }); webContents.on('did-start-navigation', (_event, _url, isSameDoc, isMainFrame) => { if (isMainFrame && !isSameDoc) { drainPendingDisplayMediaRequests('did-start-navigation'); resetStreamingPriority(); } }); registerSpellcheck(webContents); session.setDevicePermissionHandler(({deviceType, origin}) => { if (!origin || !isTrustedOrigin(origin)) { return false; } return webAuthnDeviceTypes.has(deviceType); }); session.on('select-hid-device', (event, details, callback) => { event.preventDefault(); logger.warn('Cancelling WebHID device selection request', {origin: details.frame?.url}); callback(); }); session.setPermissionRequestHandler((webContents, permission, callback, details) => { const origin = details.requestingUrl || webContents.getURL(); const trusted = isTrustedOrigin(origin); const permissionName = String(permission); if (!trusted) { if (permissionName === 'fullscreen' && isTrustedOrigin(webContents.getURL())) { callback(true); return; } callback(false); return; } if (webAuthnPermissionTypes.has(permission)) { callback(true); return; } if (trustedRendererPermissionTypes.has(permissionName)) { callback(true); return; } callback(false); }); session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { const origin = requestingOrigin || details?.requestingUrl || webContents?.getURL(); const embeddingOrigin = details?.embeddingOrigin; const permissionName = String(permission); if (!webContents) return false; if (permissionName === 'fullscreen') { const topLevel = embeddingOrigin || webContents.getURL(); if (isTrustedOrigin(topLevel)) { return true; } } if (!isTrustedOrigin(origin)) { return false; } if (embeddingOrigin && !isTrustedOrigin(embeddingOrigin)) { return false; } if (webAuthnPermissionTypes.has(permission)) { return true; } if (trustedRendererPermissionTypes.has(permissionName)) { return true; } return false; }); registerDisplayMediaRequestHandler(session, webContents); getDesktopLocalAppAuthorization().authorize(webContents); logPhase('handlers'); let appLoadFailurePrompt: AbortController | null = null; const dismissAppLoadFailurePrompt = () => { appLoadFailurePrompt?.abort(); appLoadFailurePrompt = null; }; const appLoadRetry = createAppLoadRetry({ webContents, appUrl: resolveDesktopLandingUrl(app.getPath('userData')), logger, isTrustedUrl: isTrustedOrigin, onCommitted: dismissAppLoadFailurePrompt, onRepeatedFailure: (failure) => { const window = mainWindow; if (appLoadFailurePrompt || !isAliveWindow(window) || !window.isVisible()) return; const prompt = new AbortController(); appLoadFailurePrompt = prompt; void dialog .showMessageBox(window, { type: 'warning', buttons: [t('desktop.appLoad.retry'), t('desktop.tray.quit')], defaultId: 0, cancelId: 0, title: t('desktop.appLoad.failedTitle'), message: t('desktop.appLoad.failedMessage'), detail: `${failure.errorDescription} (${failure.errorCode})\n${failure.url}`, noLink: true, signal: prompt.signal, }) .then(({response}) => { if (appLoadFailurePrompt === prompt) appLoadFailurePrompt = null; if (prompt.signal.aborted || isQuitting) return; if (response === 1) { app.quit(); return; } appLoadRetry.retryNow(); }); }, }); webContents.on('did-finish-load', () => { rendererGoneReloaded = false; mainWindowRendererGone = false; }); appLoadRetry.start(); logPhase('load-url-dispatched'); webContents.on('will-navigate', (event, url) => { preventUntrustedNavigation(event, url, true); }); webContents.on('will-frame-navigate', (event) => { preventUntrustedNavigation(event, event.url, event.isMainFrame); }); webContents.on('did-create-window', (window, details) => { if ( details.frameName === THEME_STUDIO_POPOUT_WINDOW_NAME && getSanitizedPath(details.url) === THEME_STUDIO_POPOUT_PATHNAME ) { trackThemeStudioPopoutWindow(window); } if (isVoicePopoutWindowName(details.frameName)) { trackVoicePopoutWindow(details.frameName, window); } attachChildWindowGuards(window.webContents); }); webContents.setWindowOpenHandler(({url, frameName}) => { if (isVoicePopoutWindowName(frameName) && url === 'about:blank') { if (!hasVoicePopoutCapacity()) { logger.warn('Denied voice popout window: capacity reached', {frameName}); return {action: 'deny'}; } return {action: 'allow', overrideBrowserWindowOptions: getVoicePopoutWindowOptions()}; } const pathname = getSanitizedPath(url); if ( frameName?.startsWith(POPOUT_NAMESPACE) && (pathname === '/popout' || pathname === '/quick-css-editor' || pathname === THEME_STUDIO_POPOUT_PATHNAME) && isTrustedOrigin(url) ) { const isThemeStudioPopout = frameName === THEME_STUDIO_POPOUT_WINDOW_NAME && pathname === THEME_STUDIO_POPOUT_PATHNAME; const isOpaqueChromePopout = pathname === '/quick-css-editor' || pathname === THEME_STUDIO_POPOUT_PATHNAME; const allowPopoutTransparency = allowTransparency && !isOpaqueChromePopout; const overrideBrowserWindowOptions: Electron.BrowserWindowConstructorOptions = { ...getTitleBarWindowOptions(getActiveUseNativeTitleBar()), title: isThemeStudioPopout ? THEME_STUDIO_POPOUT_TITLE : undefined, minWidth: isThemeStudioPopout ? THEME_STUDIO_POPOUT_MIN_WIDTH : undefined, minHeight: isThemeStudioPopout ? THEME_STUDIO_POPOUT_MIN_HEIGHT : undefined, ...(isMac ? {trafficLightPosition: CUSTOM_TITLEBAR_TRAFFIC_LIGHT_POSITION} : {}), backgroundColor: getWindowBackgroundColor(allowPopoutTransparency), transparent: allowPopoutTransparency, ...getWindowShadowOptions(allowPopoutTransparency), show: true, webPreferences: getSharedWebPreferences(allowPopoutTransparency, getActiveUseNativeTitleBar()), }; return {action: 'allow', overrideBrowserWindowOptions}; } openExternalDeduped(url).catch((error) => { log.warn('Failed to open external URL from window-open:', error); }); return {action: 'deny'}; }); return mainWindow; } export function showWindow(): void { if (mainWindowTakeover != null) { mainWindowTakeover(); return; } if (mainWindow && pendingMainWindowReveal?.window === mainWindow && !mainWindow.isVisible()) { pendingMainWindowReveal.requestShow(); return; } if (mainWindow) { if (mainWindow.isMinimized()) { mainWindow.restore(); } ensureWindowOnScreen(mainWindow); recoverMainWindowRendererBeforeShow('show-window'); if (process.platform === 'darwin') { try { app.dock?.show(); } catch (error) { log.warn('[Window] Failed to show dock:', error); } try { app.focus({steal: true}); } catch (error) { log.warn('[Window] Failed to focus app:', error); } try { mainWindow.setVisibleOnAllWorkspaces(true, {visibleOnFullScreen: true}); } catch (error) { log.warn('[Window] Failed to set visible on all workspaces:', error); } mainWindow.show(); mainWindow.focus(); setTimeout(() => { if (!mainWindow || mainWindow.isDestroyed()) return; try { mainWindow.setVisibleOnAllWorkspaces(false); } catch (error) { log.warn('[Window] Failed to disable visible on all workspaces:', error); } }, 250); } else { mainWindow.show(); mainWindow.focus(); } } } export function hideWindow(): void { if (mainWindow) { mainWindow.hide(); } } export function setQuitting(quitting: boolean): void { isQuitting = quitting; } export function beginMainWindowTakeover(focus: () => void): void { mainWindowTakeover = focus; } export function endMainWindowTakeover(): void { if (mainWindowTakeover == null) return; mainWindowTakeover = null; for (const listener of Array.from(takeoverEndedListeners)) { try { listener(); } catch (error) { logger.error('A main window takeover listener threw', error); } } } export function onMainWindowTakeoverEnded(listener: () => void): () => void { takeoverEndedListeners.add(listener); return () => { takeoverEndedListeners.delete(listener); }; } export function isMainWindowTakenOver(): boolean { return mainWindowTakeover != null; } export function hideAppWindowsForUpdate(keep: BrowserWindow): ReadonlyArray { const hidden: Array = []; for (const window of BrowserWindow.getAllWindows()) { if (window === keep || window.isDestroyed() || !window.isVisible()) continue; window.hide(); hidden.push(window); } return hidden; } export function restoreAppWindowsAfterUpdate(hidden: ReadonlyArray): void { for (const window of hidden) { if (window.isDestroyed()) continue; window.show(); } } export async function closeAppWindowsForUpdate(keep: BrowserWindow): Promise { const closing = BrowserWindow.getAllWindows().filter((window) => window !== keep && !window.isDestroyed()); closingMainWindowForUpdate = true; try { await Promise.all( closing.map( (window) => new Promise((resolve) => { let deadline: NodeJS.Timeout | null = null; window.once('closed', () => { if (deadline != null) clearTimeout(deadline); resolve(); }); if (!window.isClosable()) { window.destroy(); return; } deadline = setTimeout(() => { logger.warn('A window did not close for the update in time, destroying it'); if (!window.isDestroyed()) window.destroy(); }, CLOSE_FOR_UPDATE_TIMEOUT_MS); window.webContents.once('will-prevent-unload', (event) => { event.preventDefault(); }); window.close(); }), ), ); } finally { closingMainWindowForUpdate = false; } }