import { app, BrowserWindow, Tray, Menu, Notification, nativeImage, ipcMain, shell, } from 'electron'; import path from 'path'; let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; let isQuitting = false; const SERVER_URL = process.env.BACKSPACE_URL || 'http://localhost:3000'; function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 800, minWidth: 940, minHeight: 500, title: 'Backspace', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', backgroundColor: '#313338', show: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); mainWindow.loadURL(SERVER_URL); mainWindow.once('ready-to-show', () => { mainWindow?.show(); }); mainWindow.on('close', (event) => { if (!isQuitting) { event.preventDefault(); mainWindow?.hide(); } }); mainWindow.on('closed', () => { mainWindow = null; }); // Open external links in default browser mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http://') || url.startsWith('https://')) { shell.openExternal(url); } return { action: 'deny' }; }); } function createTray(): void { // Create a 16x16 tray icon (blue circle for Backspace) const size = 16; const canvas = Buffer.alloc(size * size * 4); const cx = size / 2; const cy = size / 2; const r = size / 2 - 1; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const idx = (y * size + x) * 4; const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2); if (dist <= r) { canvas[idx] = 0x58; // R (blurple) canvas[idx + 1] = 0x65; // G canvas[idx + 2] = 0xf2; // B canvas[idx + 3] = 0xff; // A } else { canvas[idx] = 0; canvas[idx + 1] = 0; canvas[idx + 2] = 0; canvas[idx + 3] = 0; } } } const icon = nativeImage.createFromBuffer(canvas, { width: size, height: size }); tray = new Tray(icon); const contextMenu = Menu.buildFromTemplate([ { label: 'Show Backspace', click: () => { mainWindow?.show(); mainWindow?.focus(); }, }, { label: 'Hide', click: () => { mainWindow?.hide(); }, }, { type: 'separator' }, { label: 'Quit', click: () => { isQuitting = true; app.quit(); }, }, ]); tray.setToolTip('Backspace'); tray.setContextMenu(contextMenu); tray.on('click', () => { if (mainWindow?.isVisible()) { mainWindow.hide(); } else { mainWindow?.show(); mainWindow?.focus(); } }); } function showNotification(title: string, body: string): void { if (Notification.isSupported()) { const notification = new Notification({ title, body, silent: false, }); notification.on('click', () => { mainWindow?.show(); mainWindow?.focus(); }); notification.show(); } } function registerIpcHandlers(): void { ipcMain.on('show-notification', (_event, data: { title: string; body: string }) => { showNotification(data.title, data.body); }); ipcMain.on('set-badge-count', (_event, count: number) => { if (app.setBadgeCount) { app.setBadgeCount(count); } }); ipcMain.on('minimize-window', () => { mainWindow?.minimize(); }); ipcMain.on('maximize-window', () => { if (mainWindow?.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow?.maximize(); } }); ipcMain.on('close-window', () => { mainWindow?.close(); }); } app.on('ready', () => { registerIpcHandlers(); createWindow(); createTray(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } else { mainWindow.show(); } }); app.on('before-quit', () => { isQuitting = true; });