import windowState from "electron-window-state" import { app, BrowserWindow, net, nativeImage, nativeTheme, protocol } from "electron" import { dirname, isAbsolute, join, relative, resolve } from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import type { TitlebarTheme } from "../preload/types" const root = dirname(fileURLToPath(import.meta.url)) const rendererRoot = join(root, "../renderer") const rendererProtocol = "oc" const rendererHost = "renderer" protocol.registerSchemesAsPrivileged([ { scheme: rendererProtocol, privileges: { secure: true, standard: true, supportFetchAPI: true, }, }, ]) let backgroundColor: string | undefined export function setBackgroundColor(color: string) { backgroundColor = color } export function getBackgroundColor(): string | undefined { return backgroundColor } function iconsDir() { return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons") } function iconPath() { const ext = process.platform === "win32" ? "ico" : "png" return join(iconsDir(), `icon.${ext}`) } function tone() { return nativeTheme.shouldUseDarkColors ? "dark" : "light" } function overlay(theme: Partial = {}) { const mode = theme.mode ?? tone() return { color: "#00000000", symbolColor: mode === "dark" ? "white" : "black", height: 40, } } export function setTitlebar(win: BrowserWindow, theme: Partial = {}) { if (process.platform !== "win32") return win.setTitleBarOverlay(overlay(theme)) } export function setDockIcon() { if (process.platform !== "darwin") return const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png")) if (!icon.isEmpty()) app.dock?.setIcon(icon) } export function createMainWindow() { const state = windowState({ defaultWidth: 1280, defaultHeight: 800, }) const mode = tone() const win = new BrowserWindow({ x: state.x, y: state.y, width: state.width, height: state.height, show: false, title: "OpenCode", icon: iconPath(), backgroundColor, ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const, trafficLightPosition: { x: 12, y: 14 }, } : {}), ...(process.platform === "win32" ? { frame: false, titleBarStyle: "hidden" as const, titleBarOverlay: overlay({ mode }), } : {}), webPreferences: { preload: join(root, "../preload/index.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }) win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { const { requestHeaders } = details upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"]) callback({ requestHeaders }) }) win.webContents.session.webRequest.onHeadersReceived((details, callback) => { const { responseHeaders = {} } = details upsertKeyValue(responseHeaders, "Access-Control-Allow-Origin", ["*"]) upsertKeyValue(responseHeaders, "Access-Control-Allow-Headers", ["*"]) callback({ responseHeaders }) }) state.manage(win) loadWindow(win, "index.html") wireZoom(win) win.once("ready-to-show", () => { win.show() }) return win } export function createLoadingWindow() { const mode = tone() const win = new BrowserWindow({ width: 640, height: 480, resizable: false, center: true, show: true, icon: iconPath(), backgroundColor, ...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}), ...(process.platform === "win32" ? { frame: false, titleBarStyle: "hidden" as const, titleBarOverlay: overlay({ mode }), } : {}), webPreferences: { preload: join(root, "../preload/index.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }) loadWindow(win, "loading.html") return win } export function registerRendererProtocol() { if (protocol.isProtocolHandled(rendererProtocol)) return protocol.handle(rendererProtocol, (request) => { const url = new URL(request.url) if (url.host !== rendererHost) { return new Response("Not found", { status: 404 }) } const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`) const rel = relative(rendererRoot, file) if (rel.startsWith("..") || isAbsolute(rel)) { return new Response("Not found", { status: 404 }) } return net.fetch(pathToFileURL(file).toString()) }) } function loadWindow(win: BrowserWindow, html: string) { const devUrl = process.env.ELECTRON_RENDERER_URL if (devUrl) { const url = new URL(html, devUrl) void win.loadURL(url.toString()) return } void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`) } function wireZoom(win: BrowserWindow) { win.webContents.setZoomFactor(1) win.webContents.on("zoom-changed", () => { win.webContents.setZoomFactor(1) }) } function upsertKeyValue(obj: Record, keyToChange: string, value: any) { const keyToChangeLower = keyToChange.toLowerCase() for (const key of Object.keys(obj)) { if (key.toLowerCase() === keyToChangeLower) { // Reassign old key obj[key] = value // Done return } } // Insert at end instead obj[keyToChange] = value }