ipc.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { execFile } from "node:child_process"
  2. import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron"
  3. import type { IpcMainEvent, IpcMainInvokeEvent } from "electron"
  4. import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types"
  5. import { getStore } from "./store"
  6. import { setTitlebar } from "./windows"
  7. const pickerFilters = (ext?: string[]) => {
  8. if (!ext || ext.length === 0) return undefined
  9. return [{ name: "Files", extensions: ext }]
  10. }
  11. type Deps = {
  12. killSidecar: () => void
  13. installCli: () => Promise<string>
  14. awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
  15. getDefaultServerUrl: () => Promise<string | null> | string | null
  16. setDefaultServerUrl: (url: string | null) => Promise<void> | void
  17. getWslConfig: () => Promise<WslConfig>
  18. setWslConfig: (config: WslConfig) => Promise<void> | void
  19. getDisplayBackend: () => Promise<string | null>
  20. setDisplayBackend: (backend: string | null) => Promise<void> | void
  21. parseMarkdown: (markdown: string) => Promise<string> | string
  22. checkAppExists: (appName: string) => Promise<boolean> | boolean
  23. wslPath: (path: string, mode: "windows" | "linux" | null) => Promise<string>
  24. resolveAppPath: (appName: string) => Promise<string | null>
  25. loadingWindowComplete: () => void
  26. runUpdater: (alertOnFail: boolean) => Promise<void> | void
  27. checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
  28. installUpdate: () => Promise<void> | void
  29. setBackgroundColor: (color: string) => void
  30. }
  31. export function registerIpcHandlers(deps: Deps) {
  32. ipcMain.handle("kill-sidecar", () => deps.killSidecar())
  33. ipcMain.handle("install-cli", () => deps.installCli())
  34. ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
  35. const send = (step: InitStep) => event.sender.send("init-step", step)
  36. return deps.awaitInitialization(send)
  37. })
  38. ipcMain.handle("get-default-server-url", () => deps.getDefaultServerUrl())
  39. ipcMain.handle("set-default-server-url", (_event: IpcMainInvokeEvent, url: string | null) =>
  40. deps.setDefaultServerUrl(url),
  41. )
  42. ipcMain.handle("get-wsl-config", () => deps.getWslConfig())
  43. ipcMain.handle("set-wsl-config", (_event: IpcMainInvokeEvent, config: WslConfig) => deps.setWslConfig(config))
  44. ipcMain.handle("get-display-backend", () => deps.getDisplayBackend())
  45. ipcMain.handle("set-display-backend", (_event: IpcMainInvokeEvent, backend: string | null) =>
  46. deps.setDisplayBackend(backend),
  47. )
  48. ipcMain.handle("parse-markdown", (_event: IpcMainInvokeEvent, markdown: string) => deps.parseMarkdown(markdown))
  49. ipcMain.handle("check-app-exists", (_event: IpcMainInvokeEvent, appName: string) => deps.checkAppExists(appName))
  50. ipcMain.handle("wsl-path", (_event: IpcMainInvokeEvent, path: string, mode: "windows" | "linux" | null) =>
  51. deps.wslPath(path, mode),
  52. )
  53. ipcMain.handle("resolve-app-path", (_event: IpcMainInvokeEvent, appName: string) => deps.resolveAppPath(appName))
  54. ipcMain.on("loading-window-complete", () => deps.loadingWindowComplete())
  55. ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
  56. ipcMain.handle("check-update", () => deps.checkUpdate())
  57. ipcMain.handle("install-update", () => deps.installUpdate())
  58. ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
  59. ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
  60. const store = getStore(name)
  61. const value = store.get(key)
  62. if (value === undefined || value === null) return null
  63. return typeof value === "string" ? value : JSON.stringify(value)
  64. })
  65. ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
  66. getStore(name).set(key, value)
  67. })
  68. ipcMain.handle("store-delete", (_event: IpcMainInvokeEvent, name: string, key: string) => {
  69. getStore(name).delete(key)
  70. })
  71. ipcMain.handle("store-clear", (_event: IpcMainInvokeEvent, name: string) => {
  72. getStore(name).clear()
  73. })
  74. ipcMain.handle("store-keys", (_event: IpcMainInvokeEvent, name: string) => {
  75. const store = getStore(name)
  76. return Object.keys(store.store)
  77. })
  78. ipcMain.handle("store-length", (_event: IpcMainInvokeEvent, name: string) => {
  79. const store = getStore(name)
  80. return Object.keys(store.store).length
  81. })
  82. ipcMain.handle(
  83. "open-directory-picker",
  84. async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
  85. const result = await dialog.showOpenDialog({
  86. properties: ["openDirectory", ...(opts?.multiple ? ["multiSelections" as const] : []), "createDirectory"],
  87. title: opts?.title ?? "Choose a folder",
  88. defaultPath: opts?.defaultPath,
  89. })
  90. if (result.canceled) return null
  91. return opts?.multiple ? result.filePaths : result.filePaths[0]
  92. },
  93. )
  94. ipcMain.handle(
  95. "open-file-picker",
  96. async (
  97. _event: IpcMainInvokeEvent,
  98. opts?: { multiple?: boolean; title?: string; defaultPath?: string; accept?: string[]; extensions?: string[] },
  99. ) => {
  100. const result = await dialog.showOpenDialog({
  101. properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
  102. title: opts?.title ?? "Choose a file",
  103. defaultPath: opts?.defaultPath,
  104. filters: pickerFilters(opts?.extensions),
  105. })
  106. if (result.canceled) return null
  107. return opts?.multiple ? result.filePaths : result.filePaths[0]
  108. },
  109. )
  110. ipcMain.handle(
  111. "save-file-picker",
  112. async (_event: IpcMainInvokeEvent, opts?: { title?: string; defaultPath?: string }) => {
  113. const result = await dialog.showSaveDialog({
  114. title: opts?.title ?? "Save file",
  115. defaultPath: opts?.defaultPath,
  116. })
  117. if (result.canceled) return null
  118. return result.filePath ?? null
  119. },
  120. )
  121. ipcMain.on("open-link", (_event: IpcMainEvent, url: string) => {
  122. void shell.openExternal(url)
  123. })
  124. ipcMain.handle("open-path", async (_event: IpcMainInvokeEvent, path: string, app?: string) => {
  125. if (!app) return shell.openPath(path)
  126. await new Promise<void>((resolve, reject) => {
  127. const [cmd, args] =
  128. process.platform === "darwin" ? (["open", ["-a", app, path]] as const) : ([app, [path]] as const)
  129. execFile(cmd, args, (err) => (err ? reject(err) : resolve()))
  130. })
  131. })
  132. ipcMain.handle("read-clipboard-image", () => {
  133. const image = clipboard.readImage()
  134. if (image.isEmpty()) return null
  135. const buffer = image.toPNG().buffer
  136. const size = image.getSize()
  137. return { buffer, width: size.width, height: size.height }
  138. })
  139. ipcMain.on("show-notification", (_event: IpcMainEvent, title: string, body?: string) => {
  140. new Notification({ title, body }).show()
  141. })
  142. ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
  143. ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
  144. const win = BrowserWindow.fromWebContents(event.sender)
  145. return win?.isFocused() ?? false
  146. })
  147. ipcMain.handle("set-window-focus", (event: IpcMainInvokeEvent) => {
  148. const win = BrowserWindow.fromWebContents(event.sender)
  149. win?.focus()
  150. })
  151. ipcMain.handle("show-window", (event: IpcMainInvokeEvent) => {
  152. const win = BrowserWindow.fromWebContents(event.sender)
  153. win?.show()
  154. })
  155. ipcMain.on("relaunch", () => {
  156. app.relaunch()
  157. app.exit(0)
  158. })
  159. ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor())
  160. ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor))
  161. ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => {
  162. const win = BrowserWindow.fromWebContents(event.sender)
  163. if (!win) return
  164. setTitlebar(win, theme)
  165. })
  166. }
  167. export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) {
  168. win.webContents.send("sqlite-migration-progress", progress)
  169. }
  170. export function sendMenuCommand(win: BrowserWindow, id: string) {
  171. win.webContents.send("menu-command", id)
  172. }
  173. export function sendDeepLinks(win: BrowserWindow, urls: string[]) {
  174. win.webContents.send("deep-link", urls)
  175. }