const { app, BrowserWindow, dialog, shell } = require('electron'); const { spawn, execFileSync } = require('child_process'); const path = require('path'); const net = require('net'); const fs = require('fs'); let phpProcess = null; let mainWindow = null; let serverPort = 0; const isDev = !app.isPackaged; function resolvePath(rel) { if (isDev) return path.join(__dirname, '..', rel); return path.join(process.resourcesPath, rel); } function findBundledPhp() { const exe = process.platform === 'win32' ? 'php.exe' : 'php'; const bundled = isDev ? path.join(__dirname, 'php-bin', process.platform, exe) : path.join(process.resourcesPath, 'php-bin', exe); if (fs.existsSync(bundled)) { try { execFileSync(bundled, ['-v'], { stdio: 'ignore', timeout: 3000 }); return bundled; } catch {} } return null; } function findSystemPhp() { const candidates = process.platform === 'win32' ? ['php.exe', 'C:\\php\\php.exe', 'C:\\xampp\\php\\php.exe'] : ['php', '/usr/bin/php', '/usr/local/bin/php', '/opt/homebrew/bin/php']; for (const bin of candidates) { try { execFileSync(bin, ['-v'], { stdio: 'ignore', timeout: 3000 }); return bin; } catch {} } return null; } function findPhp() { return findBundledPhp() || findSystemPhp(); } function checkPhpExtensions(phpBin) { try { const mods = execFileSync(phpBin, ['-m'], { timeout: 3000 }).toString().toLowerCase(); const missing = []; for (const ext of ['pdo_sqlite', 'zip']) { if (!mods.includes(ext)) missing.push(ext); } return missing; } catch { return []; } } function getFreePort() { return new Promise((resolve, reject) => { const srv = net.createServer(); srv.listen(0, '127.0.0.1', () => { const port = srv.address().port; srv.close(() => resolve(port)); }); srv.on('error', reject); }); } function getDataDir() { if (isDev) return path.join(__dirname, 'appdata'); return path.join(app.getPath('userData'), 'data'); } function ensureDataDir() { const dataDir = getDataDir(); if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); return dataDir; } async function startPhpServer() { const phpBin = findPhp(); if (!phpBin) { dialog.showErrorBox('PHP not found', 'hyprcrd needs PHP 8.1+ with pdo_sqlite.\n\n' + 'Install PHP and make sure it\'s in your PATH.\n\n' + (process.platform === 'darwin' ? 'brew install php' : process.platform === 'win32' ? 'https://windows.php.net/download/' : 'sudo apt install php php-sqlite3')); app.quit(); return null; } const missing = checkPhpExtensions(phpBin); if (missing.length) { dialog.showErrorBox('Missing PHP extensions', `hyprcrd needs these PHP extensions: ${missing.join(', ')}\n\n` + (process.platform === 'darwin' ? 'They are included with "brew install php".' : process.platform === 'win32' ? 'Enable them in php.ini (uncomment extension=...).' : `Install with: sudo apt install ${missing.map(e => 'php-' + e.replace('pdo_', '')).join(' ')}`)); app.quit(); return null; } const dataDir = ensureDataDir(); serverPort = await getFreePort(); const docRoot = resolvePath('public'); const routerFile = isDev ? path.join(__dirname, 'router.php') : path.join(process.resourcesPath, 'router.php'); console.log('[hyprcrd] PHP:', phpBin); console.log('[hyprcrd] docRoot:', docRoot); console.log('[hyprcrd] router:', routerFile); console.log('[hyprcrd] data:', dataDir); console.log('[hyprcrd] port:', serverPort); phpProcess = spawn(phpBin, [ '-S', `127.0.0.1:${serverPort}`, '-t', docRoot, routerFile, ], { cwd: resolvePath('.'), stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PHP_CLI_SERVER_WORKERS: '2', HYPRCRD_DATA_DIR: dataDir }, }); phpProcess.stderr.on('data', (d) => { const line = d.toString().trim(); if (line && !line.includes('Accepted') && !line.includes('Closing')) console.log('[php]', line); }); phpProcess.on('exit', (code) => { console.log(`[php] exited with code ${code}`); phpProcess = null; }); // Wait for the server to accept connections for (let i = 0; i < 50; i++) { const ok = await new Promise(resolve => { const sock = net.createConnection(serverPort, '127.0.0.1'); sock.on('connect', () => { sock.destroy(); resolve(true); }); sock.on('error', () => resolve(false)); }); if (ok) return serverPort; await new Promise(r => setTimeout(r, 100)); } dialog.showErrorBox('Server failed', 'PHP server did not start in time.'); app.quit(); return null; } function createWindow() { mainWindow = new BrowserWindow({ width: 1280, height: 820, minWidth: 800, minHeight: 500, title: 'hyprcrd', backgroundColor: '#0f172a', autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, }); mainWindow.loadURL(`http://127.0.0.1:${serverPort}/`); mainWindow.webContents.on('did-fail-load', (e, code, desc) => { console.log(`[hyprcrd] load failed: ${code} ${desc}`); }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http://127.0.0.1')) return { action: 'allow' }; shell.openExternal(url); return { action: 'deny' }; }); mainWindow.webContents.on('before-input-event', (e, input) => { if (input.key === 'F12') mainWindow.webContents.toggleDevTools(); if (input.key === 'r' && (input.control || input.meta)) mainWindow.webContents.reload(); }); mainWindow.on('closed', () => { mainWindow = null; }); } app.on('ready', async () => { const port = await startPhpServer(); if (port) createWindow(); }); app.on('window-all-closed', () => { app.quit(); }); app.on('before-quit', () => { if (phpProcess) { phpProcess.kill('SIGTERM'); phpProcess = null; } }); app.on('activate', () => { if (!mainWindow && serverPort) createWindow(); });