import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; import fs from 'fs'; import path from 'path'; import { spawn, ChildProcess } from 'child_process'; import { scanDirectory, getDeepDiveSummary, getDiskUsage, findHeavyFolders } from './features/scanner.js'; import { startEnforcement } from './features/enforcer.js'; import { disableAutoUpdates, ignoreUpdate } from './features/updater.js'; import { clearCaches, emptyTrash, cleanupDocker, cleanupTmp, purgePath, cleanupXcode, cleanupTurnkey } from './features/cleaner.js'; // In CJS, __dirname is globally defined. We rely on esbuild providing it. // To stop TypeScript from complaining about missing type in 'module' mode: declare const __dirname: string; declare const __filename: string; let mainWindow: BrowserWindow | null = null; let backendProcess: ChildProcess | null = null; let tray: Tray | null = null; const startBackend = () => { if (process.env.NODE_ENV === 'development') { console.log('Development mode: Backend should be running via start-go.sh'); return; } const backendPath = path.join(process.resourcesPath, 'backend'); console.log('Starting backend from:', backendPath); try { backendProcess = spawn(backendPath, [], { stdio: 'inherit' }); backendProcess.on('error', (err) => { console.error('Failed to start backend:', err); }); backendProcess.on('exit', (code, signal) => { console.log(`Backend exited with code ${code} and signal ${signal}`); }); } catch (error) { console.error('Error spawning backend:', error); } }; function createTray() { const iconPath = path.join(__dirname, '../dist/tray/tray-iconTemplate.png'); // Use Template image for automatic Light/Dark mode adjustment. // Fallback to color if needed, but Template is best for menu bar. // Check if dist/tray exists, if not try public/tray (dev mode) let finalIconPath = iconPath; if (!fs.existsSync(iconPath)) { finalIconPath = path.join(__dirname, '../public/tray/tray-iconTemplate.png'); } const image = nativeImage.createFromPath(finalIconPath); tray = new Tray(image.resize({ width: 16, height: 16 })); tray.setToolTip('Antigravity Cleaner'); updateTrayMenu('Initializing...'); } let isDockVisible = true; function updateTrayMenu(statusText: string) { if (!tray) return; const contextMenu = Menu.buildFromTemplate([ { label: `Storage: ${statusText}`, enabled: false }, { type: 'separator' }, { label: 'Open Dashboard', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } }, { label: 'Free Up Storage', click: () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); // Optionally send IPC to trigger scan? } } }, { type: 'separator' }, { label: 'Show Dock Icon', type: 'checkbox', checked: isDockVisible, click: (menuItem) => { isDockVisible = menuItem.checked; if (isDockVisible) { app.dock.show(); } else { app.dock.hide(); } } }, { type: 'separator' }, { label: 'Quit', click: () => app.quit() } ]); tray.setContextMenu(contextMenu); // Do NOT reset Title if we are just updating the menu structure, // but statusText drives the menu so we keep it. tray.setTitle(statusText); } function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, backgroundColor: '#FFFFFF', // Helps prevent white flash webPreferences: { preload: path.join(__dirname, 'preload.cjs'), nodeIntegration: true, contextIsolation: true, }, }); const isDev = process.env.NODE_ENV === 'development'; const port = process.env.PORT || 5173; if (isDev) { mainWindow.loadURL(`http://localhost:${port}`); // mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } mainWindow.on('closed', () => { mainWindow = null; }); } app.whenReady().then(() => { // IPC Handlers ipcMain.handle('scan-directory', async (event, path) => { return scanDirectory(path); }); ipcMain.handle('deep-dive-scan', async () => { return getDeepDiveSummary(); }); ipcMain.handle('get-disk-usage', async () => { return getDiskUsage(); }); ipcMain.handle('deepest-scan', async (event, targetPath) => { // Default to Documents if no path provided, or allow passing path const target = targetPath || path.join(app.getPath('home'), 'Documents'); return findHeavyFolders(target); }); ipcMain.handle('disable-updates', async () => { return disableAutoUpdates(); }); ipcMain.handle('clean-system', async () => { return clearCaches(); // and others }); ipcMain.handle('cleanup-docker', async () => { return cleanupDocker(); }); ipcMain.handle('cleanup-tmp', async () => { return cleanupTmp(); }); ipcMain.handle('cleanup-xcode', async () => { return cleanupXcode(); }); ipcMain.handle('cleanup-turnkey', async () => { return cleanupTurnkey(); }); ipcMain.handle('purge-path', async (event, targetPath) => { // Security check: ensure path is within user home or projects? // For now, allow it but in production we need safeguards. // Also import purgePath from cleaner.ts (need to update imports) // const { purgePath } = await import('./features/cleaner.js'); // Already imported above return purgePath(targetPath); }); // Start background enforcer (example: monitoring home projects) // startEnforcement('/Users/khoa.vo/Projects'); // TODO: Make configurable // IPC for Tray ipcMain.handle('update-tray-title', (event, title) => { if (tray) { tray.setTitle(title); updateTrayMenu(title); // Update menu with new status too if needed } }); ipcMain.handle('get-app-icon', async (event, appPath) => { try { const icon = await app.getFileIcon(appPath, { size: 'normal' }); return icon.toDataURL(); } catch (e) { console.error('Failed to get icon for:', appPath, e); return ''; return ''; } }); ipcMain.handle('update-tray-icon', (event, dataUrl) => { if (tray && dataUrl) { const image = nativeImage.createFromDataURL(dataUrl); // Resize to tray standard (16x16 or 22x22 usually, let's keep it crisp) tray.setImage(image.resize({ width: 22, height: 22 })); } }); createWindow(); createTray(); // Initialize Tray startBackend(); }); app.on('will-quit', () => { if (backendProcess) { console.log('Killing backend process...'); backendProcess.kill(); backendProcess = null; } }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } });