summaryrefslogtreecommitdiff
path: root/src/preload/index.ts
blob: aa168725b9a2f752a925001918fb6dd6da3cff59 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright (c) 2026 Yuren Hao
// Licensed under AGPL-3.0 - see LICENSE file

import { contextBridge, ipcRenderer, webUtils } from 'electron'
import { createHash } from 'crypto'

const api = {
  // File system
  readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
  readBinary: (path: string) => ipcRenderer.invoke('fs:readBinary', path) as Promise<ArrayBuffer>,

  // LaTeX
  onCompileLog: (cb: (log: string) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, log: string) => cb(log)
    ipcRenderer.on('latex:log', handler)
    return () => ipcRenderer.removeListener('latex:log', handler)
  },

  // Terminal (supports multiple named instances)
  ptySpawn: (id: string, cwd: string, cmd?: string, args?: string[]) => ipcRenderer.invoke('pty:spawn', id, cwd, cmd, args),
  ptyWrite: (id: string, data: string) => ipcRenderer.invoke('pty:write', id, data),
  ptyResize: (id: string, cols: number, rows: number) => ipcRenderer.invoke('pty:resize', id, cols, rows),
  ptyKill: (id: string) => ipcRenderer.invoke('pty:kill', id),
  onPtyData: (id: string, cb: (data: string) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: string) => cb(data)
    ipcRenderer.on(`pty:data:${id}`, handler)
    return () => ipcRenderer.removeListener(`pty:data:${id}`, handler)
  },
  onPtyExit: (id: string, cb: () => void) => {
    const handler = () => cb()
    ipcRenderer.on(`pty:exit:${id}`, handler)
    return () => ipcRenderer.removeListener(`pty:exit:${id}`, handler)
  },

  // SyncTeX
  synctexEdit: (pdfPath: string, page: number, x: number, y: number) =>
    ipcRenderer.invoke('synctex:editFromPdf', pdfPath, page, x, y) as Promise<{ file: string; line: number } | null>,

  // Overleaf web session (comments)
  overleafWebLogin: () => ipcRenderer.invoke('overleaf:webLogin') as Promise<{ success: boolean }>,
  overleafHasWebSession: () => ipcRenderer.invoke('overleaf:hasWebSession') as Promise<{ loggedIn: boolean }>,
  overleafGetThreads: (projectId: string) =>
    ipcRenderer.invoke('overleaf:getThreads', projectId) as Promise<{ success: boolean; threads?: Record<string, unknown>; message?: string }>,
  overleafReplyThread: (projectId: string, threadId: string, content: string) =>
    ipcRenderer.invoke('overleaf:replyThread', projectId, threadId, content) as Promise<{ success: boolean }>,
  overleafResolveThread: (projectId: string, threadId: string) =>
    ipcRenderer.invoke('overleaf:resolveThread', projectId, threadId) as Promise<{ success: boolean }>,
  overleafReopenThread: (projectId: string, threadId: string) =>
    ipcRenderer.invoke('overleaf:reopenThread', projectId, threadId) as Promise<{ success: boolean }>,
  overleafDeleteMessage: (projectId: string, threadId: string, messageId: string) =>
    ipcRenderer.invoke('overleaf:deleteMessage', projectId, threadId, messageId) as Promise<{ success: boolean }>,
  overleafEditMessage: (projectId: string, threadId: string, messageId: string, content: string) =>
    ipcRenderer.invoke('overleaf:editMessage', projectId, threadId, messageId, content) as Promise<{ success: boolean }>,
  overleafDeleteThread: (projectId: string, docId: string, threadId: string) =>
    ipcRenderer.invoke('overleaf:deleteThread', projectId, docId, threadId) as Promise<{ success: boolean }>,
  overleafAddComment: (projectId: string, docId: string, pos: number, text: string, content: string) =>
    ipcRenderer.invoke('overleaf:addComment', projectId, docId, pos, text, content) as Promise<{ success: boolean; threadId?: string; message?: string }>,

  // OT / Socket mode
  otConnect: (projectId: string) =>
    ipcRenderer.invoke('ot:connect', projectId) as Promise<{
      success: boolean
      files?: unknown[]
      project?: { name: string; rootDocId: string }
      docPathMap?: Record<string, string>
      pathDocMap?: Record<string, string>
      fileRefs?: Array<{ id: string; path: string }>
      rootFolderId?: string
      message?: string
    }>,
  otDisconnect: () => ipcRenderer.invoke('ot:disconnect'),
  otJoinDoc: (docId: string) =>
    ipcRenderer.invoke('ot:joinDoc', docId) as Promise<{
      success: boolean
      content?: string
      version?: number
      ranges?: { comments: Array<{ id: string; op: { c: string; p: number; t: string } }>; changes: unknown[] }
      message?: string
    }>,
  otLeaveDoc: (docId: string) => ipcRenderer.invoke('ot:leaveDoc', docId),
  otSendOp: (docId: string, ops: unknown[], version: number, hash: string) =>
    ipcRenderer.invoke('ot:sendOp', docId, ops, version, hash),
  otFetchAllCommentContexts: () =>
    ipcRenderer.invoke('ot:fetchAllCommentContexts') as Promise<{
      success: boolean
      contexts?: Record<string, { file: string; text: string; pos: number }>
    }>,
  onOtRemoteOp: (cb: (data: { docId: string; ops: unknown[]; version: number }) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: { docId: string; ops: unknown[]; version: number }) => cb(data)
    ipcRenderer.on('ot:remoteOp', handler)
    return () => ipcRenderer.removeListener('ot:remoteOp', handler)
  },
  onOtAck: (cb: (data: { docId: string }) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: { docId: string }) => cb(data)
    ipcRenderer.on('ot:ack', handler)
    return () => ipcRenderer.removeListener('ot:ack', handler)
  },
  onOtConnectionState: (cb: (state: string) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, state: string) => cb(state)
    ipcRenderer.on('ot:connectionState', handler)
    return () => ipcRenderer.removeListener('ot:connectionState', handler)
  },
  onOtDocRejoined: (cb: (data: { docId: string; content: string; version: number }) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: { docId: string; content: string; version: number }) => cb(data)
    ipcRenderer.on('ot:docRejoined', handler)
    return () => ipcRenderer.removeListener('ot:docRejoined', handler)
  },
  overleafListProjects: () =>
    ipcRenderer.invoke('overleaf:listProjects') as Promise<{
      success: boolean
      projects?: Array<{
        id: string; name: string; lastUpdated: string
        owner?: { firstName: string; lastName: string; email?: string }
        lastUpdatedBy?: { firstName: string; lastName: string } | null
        accessLevel?: string; source?: string
      }>
      message?: string
    }>,
  overleafCreateProject: (name: string) =>
    ipcRenderer.invoke('overleaf:createProject', name) as Promise<{
      success: boolean; projectId?: string; message?: string
    }>,
  overleafUploadProject: () =>
    ipcRenderer.invoke('overleaf:uploadProject') as Promise<{
      success: boolean; projectId?: string; message?: string
    }>,
  overleafSocketCompile: (mainTexRelPath: string) =>
    ipcRenderer.invoke('overleaf:socketCompile', mainTexRelPath) as Promise<{
      success: boolean; log: string; pdfPath: string
    }>,
  overleafRenameEntity: (projectId: string, entityType: string, entityId: string, newName: string) =>
    ipcRenderer.invoke('overleaf:renameEntity', projectId, entityType, entityId, newName) as Promise<{ success: boolean; message?: string }>,
  overleafDeleteEntity: (projectId: string, entityType: string, entityId: string) =>
    ipcRenderer.invoke('overleaf:deleteEntity', projectId, entityType, entityId) as Promise<{ success: boolean; message?: string }>,
  overleafCreateDoc: (projectId: string, parentFolderId: string, name: string) =>
    ipcRenderer.invoke('overleaf:createDoc', projectId, parentFolderId, name) as Promise<{ success: boolean; data?: unknown; message?: string }>,
  overleafCreateFolder: (projectId: string, parentFolderId: string, name: string) =>
    ipcRenderer.invoke('overleaf:createFolder', projectId, parentFolderId, name) as Promise<{ success: boolean; data?: unknown; message?: string }>,
  uploadFileToProject: (projectId: string, folderId: string, filePath: string, fileName: string) =>
    ipcRenderer.invoke('project:uploadFile', projectId, folderId, filePath, fileName) as Promise<{ success: boolean; message?: string }>,
  getPathForFile: (file: File) => webUtils.getPathForFile(file),
  sha1: (text: string): string => createHash('sha1').update(text).digest('hex'),

  // File sync bridge
  onSyncExternalEdit: (cb: (data: { docId: string; content: string }) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: { docId: string; content: string }) => cb(data)
    ipcRenderer.on('sync:externalEdit', handler)
    return () => ipcRenderer.removeListener('sync:externalEdit', handler)
  },
  syncContentChanged: (docId: string, content: string) =>
    ipcRenderer.invoke('sync:contentChanged', docId, content),

  // Cursor tracking
  cursorUpdate: (docId: string, row: number, column: number) =>
    ipcRenderer.invoke('cursor:update', docId, row, column),
  cursorGetConnectedUsers: () =>
    ipcRenderer.invoke('cursor:getConnectedUsers') as Promise<unknown[]>,
  onCursorRemoteUpdate: (cb: (data: unknown) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, data: unknown) => cb(data)
    ipcRenderer.on('cursor:remoteUpdate', handler)
    return () => ipcRenderer.removeListener('cursor:remoteUpdate', handler)
  },
  onCursorRemoteDisconnected: (cb: (clientId: string) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, clientId: string) => cb(clientId)
    ipcRenderer.on('cursor:remoteDisconnected', handler)
    return () => ipcRenderer.removeListener('cursor:remoteDisconnected', handler)
  },

  // Chat
  chatGetMessages: (projectId: string, limit?: number) =>
    ipcRenderer.invoke('chat:getMessages', projectId, limit) as Promise<{ success: boolean; messages: unknown[] }>,
  chatSendMessage: (projectId: string, content: string) =>
    ipcRenderer.invoke('chat:sendMessage', projectId, content) as Promise<{ success: boolean }>,
  onChatMessage: (cb: (msg: unknown) => void) => {
    const handler = (_e: Electron.IpcRendererEvent, msg: unknown) => cb(msg)
    ipcRenderer.on('chat:newMessage', handler)
    return () => ipcRenderer.removeListener('chat:newMessage', handler)
  },

  // Shell
  openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
  showInFinder: (path: string) => ipcRenderer.invoke('shell:showInFinder', path)
}

contextBridge.exposeInMainWorld('api', api)

export type ElectronAPI = typeof api