import { execFile } from "node:child_process" import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron" import type { IpcMainEvent, IpcMainInvokeEvent } from "electron" import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types" import { getStore } from "./store" import { setTitlebar } from "./windows" type Deps = { killSidecar: () => void installCli: () => Promise awaitInitialization: (sendStep: (step: InitStep) => void) => Promise getDefaultServerUrl: () => Promise | string | null setDefaultServerUrl: (url: string | null) => Promise | void getWslConfig: () => Promise setWslConfig: (config: WslConfig) => Promise | void getDisplayBackend: () => Promise setDisplayBackend: (backend: string | null) => Promise | void parseMarkdown: (markdown: string) => Promise | string checkAppExists: (appName: string) => Promise | boolean wslPath: (path: string, mode: "windows" | "linux" | null) => Promise resolveAppPath: (appName: string) => Promise loadingWindowComplete: () => void runUpdater: (alertOnFail: boolean) => Promise | void checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> installUpdate: () => Promise | void setBackgroundColor: (color: string) => void } export function registerIpcHandlers(deps: Deps) { ipcMain.handle("kill-sidecar", () => deps.killSidecar()) ipcMain.handle("install-cli", () => deps.installCli()) ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => { const send = (step: InitStep) => event.sender.send("init-step", step) return deps.awaitInitialization(send) }) ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl()) ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) => deps.setDefaultServerUrl(url), ) ipcMain.handle("get-wsl-config", () => deps.getWslConfig()) ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config)) ipcMain.handle("get-display-backend", () => deps.getDisplayBackend()) ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) => deps.setDisplayBackend(backend), ) ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown)) ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName)) ipcMain.handle("wsl-path", (_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null) => deps.wslPath(path, mode), ) ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName)) ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete()) ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail)) ipcMain.handle("check-update", () => deps.checkUpdate()) ipcMain.handle("install-update", () => deps.installUpdate()) ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color)) ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => { const store = getStore(name) const value = store.get(key) if (value === undefined || value === null) return null return typeof value === "string" ? value : JSON.stringify(value) }) ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => { getStore(name).set(key, value) }) ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => { getStore(name).delete(key) }) ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => { getStore(name).clear() }) ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => { const store = getStore(name) return Object.keys(store.store) }) ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => { const store = getStore(name) return Object.keys(store.store).length }) ipcMain.handle( "open-directory-picker", async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => { const result = await dialog.showOpenDialog({ properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : [])], title: opts?.title ?? "Choose a folder", defaultPath: opts?.defaultPath, }) if (result.canceled) return null return opts?.multiple ? result.filePaths : result.filePaths[0] }, ) ipcMain.handle( "open-file-picker", async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => { const result = await dialog.showOpenDialog({ properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])], title: opts?.title ?? "Choose a file", defaultPath: opts?.defaultPath, }) if (result.canceled) return null return opts?.multiple ? result.filePaths : result.filePaths[0] }, ) ipcMain.handle( "save-file-picker", async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => { const result = await dialog.showSaveDialog({ title: opts?.title ?? "Save file", defaultPath: opts?.defaultPath, }) if (result.canceled) return null return result.filePath ?? null }, ) ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => { void shell.openExternal(url) }) ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => { if (!app) return shell.openPath(path) await new Promise((resolve, reject) => { const [cmd, args] = process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const) execFile(cmd, args, (err) => (err ? reject(err) : resolve())) }) }) ipcMain.handle("read-clipboard-image", () => { const image = clipboard.readImage() if (image.isEmpty()) return null const buffer = image.toPNG().buffer const size = image.getSize() return { buffer, width: size.width, height: size.height } }) ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => { new Notification({ title, body }).show() }) ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length) ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) return win?.isFocused() ?? false }) ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) win?.focus() }) ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => { const win = BrowserWindow.fromWebContents(event.sender) win?.show() }) ipcMain.on("relaunch", () => { app.relaunch() app.exit(0) }) ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor()) ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor)) ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return setTitlebar(win, theme) }) } export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) { win.webContents.send("sqlite-migration-progress", progress) } export function sendMenuCommand(win: BrowserWindow, id: string) { win.webContents.send("menu-command", id) } export function sendDeepLinks(win: BrowserWindow, urls: string[]) { win.webContents.send("deep-link", urls) }