Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
286 lines (249 loc) · 10 KB
/
Copy pathmain.js
File metadata and controls
286 lines (249 loc) · 10 KB
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
/**
* ==============================================================================
* MAIN PROCESS ENTRY POINT
* ==============================================================================
*
* This file is the heart of the Electron application. It acts as the "Backend"
* or "Server" side of your desktop app.
*
* Responsibilities:
* 1. Initialize the application.
* 2. Set up the "Overlay" window where the fish lives.
* 3. Manage background services (Clipboard monitoring, AI processing).
* 4. Handle communication (IPC) between the invisible backend and the visible UI.
*/
require('dotenv').config({ path: './.env' }) // Load secret keys (like your AI API Key)
const { app, ipcMain, globalShortcut } = require('electron')
// Import our custom service modules
const Config = require('./electron/services/config')
const AIService = require('./electron/services/aiService')
const ClipboardService = require('./electron/services/clipboardService')
const ContextService = require('./electron/services/contextService')
const TrayService = require('./electron/services/trayService')
const LoggerService = require('./electron/services/loggerService')
const OCRService = require('./electron/services/ocrService')
const WindowManager = require('./electron/windows/windowManager')
/**
* The main Application class that orchestrates everything.
*/
class App {
constructor() {
// Hold references to our services
this.config = Config
this.windowManager = null
this.clipboardService = null
this.aiService = null
this.trayService = null
this.contextService = null
this.loggerService = null
this.ocrService = null
}
/**
* Step 1: Boot up the application
*/
async initialize() {
try {
// 1. Check if we have all necessary settings (like API keys)
this.config.validate()
// 2. Create instances of our services
this.initializeServices()
// 3. Set up the "listeners" for events (like clipboard changes)
this.setupIPCHandling()
// 4. Tell Electron to start the actual program
await this.start()
} catch (error) {
console.error('❌ CRITICAL ERROR: Failed to initialize app:', error)
process.exit(1) // Stop the app if we can't start
}
}
/**
* Create the service objects that power the app features.
*/
initializeServices() {
this.windowManager = new WindowManager(this.config)
this.aiService = new AIService(this.config)
this.clipboardService = new ClipboardService(this.config)
this.trayService = new TrayService(this.config)
this.contextService = new ContextService(this.config)
this.loggerService = new LoggerService()
this.ocrService = new OCRService(this.config)
}
/**
* Centralized function to handle AI requests from any source (Clipboard, Shortcut, etc.)
*/
async handleAIRequest(text, source) {
const mainWindow = this.windowManager.getMainWindow()
if (!mainWindow || mainWindow.isDestroyed()) return
// 0. Log the trigger
console.log(`⚡️ Triggered by ${source}`)
this.loggerService.log('TRIGGER', { source, textLength: text.length })
// 1. Send tray bounds & text to UI
const trayBounds = this.trayService.getBounds()
if (trayBounds) mainWindow.webContents.send('tray-bounds', trayBounds)
mainWindow.webContents.send('clipboard-update', text)
try {
// 2. Get Context
const context = this.contextService.getContext()
this.loggerService.log('CONTEXT', context)
// 3. Analyze
const analysis = await this.aiService.analyzeText(text, context)
// 4. Respond
mainWindow.webContents.send('ai-response', analysis)
this.loggerService.log('RESPONSE', { suggestion: analysis.suggestion })
} catch (error) {
console.error('AI Error:', error)
this.loggerService.log('ERROR', { message: error.message })
mainWindow.webContents.send('ai-response', {
suggestion: `AI Error: ${error.message}`,
confidence: 0,
originalText: text,
error: true
})
}
}
/**
* IPC = Inter-Process Communication.
* This is how the "Backend" (Main Process) talks to the "Frontend" (React UI).
*/
setupIPCHandling() {
// ------------------------------------------------------------------------
// EVENT: Mouse Pass-Through
// ------------------------------------------------------------------------
ipcMain.on('set-mouse-passthrough', (event, passthrough) => {
const mainWindow = this.windowManager.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setIgnoreMouseEvents(passthrough, { forward: true })
}
})
// ------------------------------------------------------------------------
// EVENT: Quit App
// ------------------------------------------------------------------------
ipcMain.on('quit-app', () => {
app.quit()
})
// ------------------------------------------------------------------------
// EVENT: Open External URL in default browser
// ------------------------------------------------------------------------
ipcMain.on('open-external', (event, url) => {
const { shell } = require('electron')
shell.openExternal(url)
})
// ------------------------------------------------------------------------
// EVENT: Open ICS file directly in Apple Calendar
// ------------------------------------------------------------------------
ipcMain.handle('open-ics-file', async (event, icsContent, filename) => {
const fs = require('fs')
const path = require('path')
const os = require('os')
const { exec } = require('child_process')
try {
// Create temp file path
const tempDir = os.tmpdir()
const safeFilename = filename.replace(/[^a-z0-9_-]/gi, '_') + '.ics'
const filePath = path.join(tempDir, safeFilename)
// Write ICS content to temp file
fs.writeFileSync(filePath, icsContent, 'utf8')
// Use AppleScript to:
// 1. Open the .ics file
// 2. Activate Calendar app (brings to foreground like shell.openExternal does for Chrome)
const appleScript = `
do shell script "open '${filePath}'"
delay 0.5
tell application "Calendar" to activate
`
return new Promise((resolve) => {
exec(`osascript -e '${appleScript.replace(/'/g, "'\\''")}'`, (error) => {
if (error) {
// Fallback: try simpler approach
exec(`open "${filePath}" && osascript -e 'tell application "Calendar" to activate'`, (err2) => {
if (err2) {
console.error('Failed to open ICS file:', err2)
resolve({ success: false, error: err2.message })
} else {
resolve({ success: true })
}
})
} else {
resolve({ success: true })
}
})
})
} catch (error) {
console.error('Error opening ICS file:', error)
return { success: false, error: error.message }
}
})
// ------------------------------------------------------------------------
// SERVICE: Clipboard Monitoring
// ------------------------------------------------------------------------
// We are disabling auto-trigger on copy for now.
// The user prefers manual trigger via Cmd+Option+F.
/*
this.clipboardService.start(async (text) => {
await this.handleAIRequest(text, 'CLIPBOARD')
})
*/
}
/**
* Step 4: Handle Electron Lifecycle Events (Start, Close, Quit)
*/
async start() {
// Called when Electron is fully ready to show windows
app.whenReady().then(() => {
try {
const mainWindow = this.windowManager.createOverlayWindow()
this.trayService.initialize(mainWindow) // Create the system tray icon
// REGISTER GLOBAL SHORTCUT: Cmd+Option+F (Fish)
// Note: 'Alt' maps to 'Option' on macOS
globalShortcut.register('CommandOrControl+Alt+F', async () => {
// When shortcut is pressed, capture screen text via OCR
console.log('⚡️ Triggered by SHORTCUT')
try {
// Try OCR first, fall back to clipboard if OCR fails
let text;
if (this.ocrService.isAvailable()) {
console.log('📸 Running screen OCR...')
text = await this.ocrService.captureAndExtract()
console.log(`📝 OCR extracted ${text.length} characters`)
} else {
console.log('⚠️ OCR not available, using clipboard')
const { clipboard } = require('electron')
text = clipboard.readText()
}
this.handleAIRequest(text, 'SHORTCUT_OCR')
} catch (error) {
console.error('OCR Error:', error)
// Fall back to clipboard
const { clipboard } = require('electron')
const text = clipboard.readText()
this.handleAIRequest(text, 'SHORTCUT_CLIPBOARD_FALLBACK')
}
})
console.log('✅ LazyFish started successfully!')
console.log('📝 Logs at:', this.loggerService.getLogPath())
} catch (error) {
console.error('Failed to create window:', error)
app.quit()
}
})
// Quit when all windows are closed (standard behavior)
app.on('window-all-closed', this.windowManager.onWindowAllClosed(() => app.quit()))
// On macOS, clicking the dock icon should recreate the window if it's gone
app.on('activate', this.windowManager.onActivate(() => {
const mainWindow = this.windowManager.createOverlayWindow()
this.trayService.initialize(mainWindow)
}))
// Start monitoring context (active window)
this.contextService.start()
// Clean up services before quitting
app.on('before-quit', () => {
globalShortcut.unregisterAll()
this.clipboardService.stop()
this.contextService.stop()
this.trayService.destroy()
})
}
}
// 🚀 Launch the application!
const lazyFish = new App()
lazyFish.initialize()