import { contextBridge, clipboard, ipcRenderer } from 'electron' /** * The single bridge between renderer and main. The renderer NEVER touches the * filesystem, git or the OS clipboard directly — everything goes through here. * (PTYs land here next, for the terminals.) */ const api = { platform: process.platform, clipboard: { writeText: (text: string) => clipboard.writeText(text), readText: (): string => clipboard.readText(), }, project: { current: () => ipcRenderer.invoke('project:current'), open: () => ipcRenderer.invoke('project:open'), openPath: (path: string) => ipcRenderer.invoke('project:openPath', path), recent: (): Promise<{ path: string; name: string }[]> => ipcRenderer.invoke('projects:recent'), }, fs: { tree: () => ipcRenderer.invoke('fs:tree'), readDir: (path: string) => ipcRenderer.invoke('fs:readDir', path), files: () => ipcRenderer.invoke('fs:files'), read: (path: string) => ipcRenderer.invoke('fs:read', path), imageDataUrl: (path: string): Promise => ipcRenderer.invoke('fs:imageDataUrl', path), write: (path: string, content: string): Promise => ipcRenderer.invoke('fs:write', path, content), delete: (path: string): Promise => ipcRenderer.invoke('fs:delete', path), create: (path: string): Promise => ipcRenderer.invoke('fs:create', path), mkdir: (path: string): Promise => ipcRenderer.invoke('fs:mkdir', path), rename: (path: string, name: string): Promise => ipcRenderer.invoke('fs:rename', path, name), }, shell: { reveal: (path: string): void => { ipcRenderer.invoke('shell:reveal', path) }, }, notes: { read: (): Promise => ipcRenderer.invoke('notes:read'), write: (text: string): Promise => ipcRenderer.invoke('notes:write', text), }, git: { load: () => ipcRenderer.invoke('git:load'), stage: (paths: string[]) => ipcRenderer.invoke('git:stage', paths), unstage: (paths: string[]) => ipcRenderer.invoke('git:unstage', paths), commit: (message: string) => ipcRenderer.invoke('git:commit', message), push: (): Promise<{ ok: boolean; message: string }> => ipcRenderer.invoke('git:push'), discard: (paths: string[]) => ipcRenderer.invoke('git:discard', paths), }, pty: { available: (): Promise => ipcRenderer.invoke('pty:available'), create: (kind: 'agent' | 'shell', cols: number, rows: number): Promise => ipcRenderer.invoke('pty:create', kind, cols, rows), write: (id: number, data: string): void => ipcRenderer.send('pty:write', id, data), resize: (id: number, cols: number, rows: number): void => ipcRenderer.send('pty:resize', id, cols, rows), kill: (id: number): void => ipcRenderer.send('pty:kill', id), onData: (cb: (id: number, data: string) => void): (() => void) => { const h = (_e: unknown, p: { id: number; data: string }): void => cb(p.id, p.data) ipcRenderer.on('pty:data', h) return () => ipcRenderer.removeListener('pty:data', h) }, onExit: (cb: (id: number) => void): (() => void) => { const h = (_e: unknown, p: { id: number }): void => cb(p.id) ipcRenderer.on('pty:exit', h) return () => ipcRenderer.removeListener('pty:exit', h) }, }, config: { get: () => ipcRenderer.invoke('config:get'), theme: (): Promise => ipcRenderer.invoke('config:theme'), }, recent: { get: (): Promise => ipcRenderer.invoke('recent:get'), set: (list: string[]): Promise => ipcRenderer.invoke('recent:set', list), }, search: { content: (query: string) => ipcRenderer.invoke('search:content', query), files: (): Promise => ipcRenderer.invoke('search:files'), }, symbols: { names: (): Promise => ipcRenderer.invoke('symbols:names'), lookup: (name: string) => ipcRenderer.invoke('symbols:lookup', name), }, dialog: { unsavedClose: (path: string): Promise<'save' | 'discard' | 'cancel'> => ipcRenderer.invoke('dialog:unsavedClose', path), }, /** Renderer errors → the main process log file (see renderer/src/log.ts). * `write` is fire-and-forget on purpose: logging must never await, and must * never be able to reject into the very handler that's reporting a failure. */ log: { write: (level: 'debug' | 'info' | 'warn' | 'error', scope: string, msg: string, ctx?: unknown): void => { ipcRenderer.send('log:write', level, scope, msg, ctx) }, path: (): Promise => ipcRenderer.invoke('log:path'), open: (): Promise => ipcRenderer.invoke('log:open'), reveal: (): Promise => ipcRenderer.invoke('log:reveal'), }, /** Subscribe to "the project changed on disk" pings. Returns an unsubscribe. */ onProjectChanged: (cb: () => void): (() => void) => { const handler = (): void => cb() ipcRenderer.on('project:changed', handler) return () => ipcRenderer.removeListener('project:changed', handler) }, /** Subscribe to .helder config/theme edits. Returns an unsubscribe. */ onConfigChanged: (cb: () => void): (() => void) => { const handler = (): void => cb() ipcRenderer.on('config:changed', handler) return () => ipcRenderer.removeListener('config:changed', handler) }, /** Subscribe to the window entering/leaving fullscreen. Returns an unsubscribe. */ onFullscreen: (cb: (on: boolean) => void): (() => void) => { const handler = (_e: unknown, on: boolean): void => cb(on) ipcRenderer.on('window:fullscreen', handler) return () => ipcRenderer.removeListener('window:fullscreen', handler) }, /** Subscribe to explicit ⌘R refresh requests (git + tree + viewer). Returns an unsubscribe. */ onRefresh: (cb: () => void): (() => void) => { const handler = (): void => cb() ipcRenderer.on('view:refresh', handler) return () => ipcRenderer.removeListener('view:refresh', handler) }, } if (process.contextIsolated) { contextBridge.exposeInMainWorld('helder', api) } else { ;(globalThis as unknown as { helder: typeof api }).helder = api } export type HelderApi = typeof api