| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- import { execFile } from "node:child_process"
- import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
- import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
- import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
- import type {
- FatalRendererError,
- ServerReadyData,
- TitlebarTheme,
- WindowConfig,
- WslConfig,
- } from "../preload/types"
- import { runDesktopMenuAction } from "./desktop-menu-actions"
- import { getStore } from "./store"
- import { getPinchZoomEnabled, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
- const pickerFilters = (ext?: string[]) => {
- if (!ext || ext.length === 0) return undefined
- return [{ name: "Files", extensions: ext }]
- }
- type Deps = {
- killSidecar: () => Promise<void> | void
- awaitInitialization: () => Promise<ServerReadyData>
- getWindowConfig: () => Promise<WindowConfig> | WindowConfig
- consumeInitialDeepLinks: () => Promise<string[]> | string[]
- getDefaultServerUrl: () => Promise<string | null> | string | null
- setDefaultServerUrl: (url: string | null) => Promise<void> | void
- getWslConfig: () => Promise<WslConfig>
- setWslConfig: (config: WslConfig) => Promise<void> | void
- getDisplayBackend: () => Promise<string | null>
- setDisplayBackend: (backend: string | null) => Promise<void> | void
- parseMarkdown: (markdown: string) => Promise<string> | string
- checkAppExists: (appName: string) => Promise<boolean> | boolean
- wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
- resolveAppPath: (appName: string) => Promise<string | null>
- runUpdater: (alertOnFail: boolean) => Promise<void> | void
- checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
- installUpdate: () => Promise<void> | void
- setBackgroundColor: (color: string) => void
- exportDebugLogs: () => Promise<string>
- recordFatalRendererError: (error: FatalRendererError) => Promise<void> | void
- }
- export function registerIpcHandlers(deps: Deps) {
- ipcMain.handle("kill-sidecar", () => deps.killSidecar())
- ipcMain.handle("await-initialization", () => deps.awaitInitialization())
- ipcMain.handle("get-window-config", () => deps.getWindowConfig())
- ipcMain.handle("consume-initial-deep-links", () => deps.consumeInitialDeepLinks())
- 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.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("export-debug-logs", () => deps.exportDebugLogs())
- ipcMain.handle("record-fatal-renderer-error", (_event: IpcMainInvokeEvent, error: FatalRendererError) =>
- deps.recordFatalRendererError(error),
- )
- ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
- try {
- const store = getStore(name)
- const value = store.get(key)
- if (value === undefined || value === null) return null
- return typeof value === "string" ? value : JSON.stringify(value)
- } catch {
- return null
- }
- })
- 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] : []), "createDirectory"],
- 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; accept?: string[]; extensions?: string[] },
- ) => {
- const result = await dialog.showOpenDialog({
- properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
- title: opts?.title ?? "Choose a file",
- defaultPath: opts?.defaultPath,
- filters: pickerFilters(opts?.extensions),
- })
- 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<void>((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)
- const win = BrowserWindow.fromWebContents(event.sender)
- if (!win) return
- updateTitlebar(win)
- })
- ipcMain.handle("get-pinch-zoom-enabled", () => getPinchZoomEnabled())
- ipcMain.handle("set-pinch-zoom-enabled", (_event: IpcMainInvokeEvent, enabled: boolean) => {
- setPinchZoomEnabled(enabled)
- })
- ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
- const win = BrowserWindow.fromWebContents(event.sender)
- if (!win) return
- setTitlebar(win, theme)
- })
- ipcMain.handle("run-desktop-menu-action", (event: IpcMainInvokeEvent, action: DesktopMenuAction) => {
- runDesktopMenuAction(BrowserWindow.fromWebContents(event.sender), action)
- })
- }
- 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)
- }
|