Adding Electron APIs
Electron APIs allow the frontend to call Electron main process functionality directly via IPC.
Four Files to Edit
-
frontend/types/custom.d.ts
-
TypeScript ElectronApi type
-
emain/preload.ts
-
Expose method via contextBridge
-
emain/emain-ipc.ts
-
Implement IPC handler
-
frontend/preview/preview-electron-api.ts
-
Add a no-op stub to keep the previewElectronApi object in sync with the ElectronApi type
Three Communication Patterns
- Sync - ipcRenderer.sendSync()
- ipcMain.on()
- event.returnValue = ...
- Async - ipcRenderer.invoke()
- ipcMain.handle()
- Fire-and-forget - ipcRenderer.send()
- ipcMain.on()
Example: Async Method
- Define TypeScript Interface
In frontend/types/custom.d.ts :
type ElectronApi = { captureScreenshot: (rect: Electron.Rectangle) => Promise<string>; // capture-screenshot };
- Expose in Preload
In emain/preload.ts :
contextBridge.exposeInMainWorld("api", { captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect), });
- Implement Handler
In emain/emain-ipc.ts :
electron.ipcMain.handle("capture-screenshot", async (event, rect) => {
const tabView = getWaveTabViewByWebContentsId(event.sender.id);
if (!tabView) throw new Error("No tab view found");
const image = await tabView.webContents.capturePage(rect);
return data:image/png;base64,${image.toPNG().toString("base64")};
});
- Add Preview Stub
In frontend/preview/preview-electron-api.ts :
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
- Call from Frontend
import { getApi } from "@/store/global";
const dataUrl = await getApi().captureScreenshot({ x: 0, y: 0, width: 800, height: 600 });
Example: Sync Method
- Define
type ElectronApi = { getUserName: () => string; // get-user-name };
- Preload
getUserName: () => ipcRenderer.sendSync("get-user-name"),
- Handler (⚠️ MUST set event.returnValue or browser hangs)
electron.ipcMain.on("get-user-name", (event) => { event.returnValue = process.env.USER || "unknown"; });
- Call
import { getApi } from "@/store/global";
const userName = getApi().getUserName(); // blocks until returns
Example: Fire-and-Forget
- Define
type ElectronApi = { openExternal: (url: string) => void; // open-external };
- Preload
openExternal: (url) => ipcRenderer.send("open-external", url),
- Handler
electron.ipcMain.on("open-external", (event, url) => { electron.shell.openExternal(url); });
Example: Event Listener
- Define
type ElectronApi = { onZoomFactorChange: (callback: (zoomFactor: number) => void) => void; // zoom-factor-change };
- Preload
onZoomFactorChange: (callback) => ipcRenderer.on("zoom-factor-change", (_event, zoomFactor) => callback(zoomFactor)),
- Send from Main
webContents.send("zoom-factor-change", newZoomFactor);
Quick Reference
Use Sync when:
-
Getting config/env vars
-
Quick lookups, no I/O
-
⚠️ CRITICAL: Always set event.returnValue or browser hangs
Use Async when:
-
File operations
-
Network requests
-
Can fail or take time
Use Fire-and-forget when:
-
No return value needed
-
Triggering actions
Electron API vs RPC:
-
Electron API: Native OS features, window management, Electron APIs
-
RPC: Database, backend logic, remote servers
Checklist
-
Add to ElectronApi in custom.d.ts
-
Include IPC channel name in comment
-
Expose in preload.ts
-
Implement in emain-ipc.ts
-
Add no-op stub to preview-electron-api.ts
-
IPC channel names match exactly
-
For sync: Set event.returnValue (or browser hangs!)
-
Test end-to-end