diff --git a/ac-electron/main.js b/ac-electron/main.js --- a/ac-electron/main.js +++ b/ac-electron/main.js @@ -7,23 +7,16 @@ * - Development windows: Webview to localhost:8888 (orange accent) * - Shell windows: Terminal with devcontainer + emacs (purple accent) */ -const { app, BrowserWindow, ipcMain, globalShortcut, Menu, Tray, dialog, shell, nativeImage, screen, Notification } = require('electron'); +const { app, BrowserWindow, ipcMain, globalShortcut, Menu, Tray, dialog, shell, nativeImage, screen, Notification, net } = require('electron'); const path = require('path'); const fs = require('fs'); const { spawn, execSync } = require('child_process'); -// Workaround for crash on macOS 26 (Tahoe) with fontations (Rust font renderer) -// The crash occurs in fontations_ffi when loading complex WebGL pieces (like 1v1) -// NUCLEAR OPTION: Completely disable GPU to force software rendering -// This bypasses the fontations Rust font renderer crash entirely -app.commandLine.appendSwitch('disable-gpu'); -app.commandLine.appendSwitch('disable-software-rasterizer'); -// Disable ALL font-related Chromium features as backup -app.commandLine.appendSwitch('disable-features', 'FontationsFontBackend,Fontations,UseSkiaFontManager,SkiaFontManager,HarfBuzzFontShaper,GpuRasterization'); -app.commandLine.appendSwitch('disable-font-subpixel-positioning'); -app.commandLine.appendSwitch('disable-lcd-text'); -// Disable GPU compositing - use CPU compositing -app.commandLine.appendSwitch('disable-gpu-compositing'); +// macOS Tahoe + Chromium fontations workaround (testing with Electron 39 / Chromium M142) +// Disable problematic font features that may trigger fontations_ffi crash +app.commandLine.appendSwitch('disable-features', 'FontationsFontBackend,Fontations'); +// Use Metal for GPU acceleration on macOS +app.commandLine.appendSwitch('use-angle', 'metal'); // Preferences storage const PREFS_PATH = path.join(app.getPath('userData'), 'preferences.json'); @@ -57,7 +50,14 @@ // Auto-updater (only in production builds) let autoUpdater; let autoUpdaterError = null; let updateDownloaded = false; +let updateAvailable = null; // { version, url, releaseNotes } +let trayBlinkInterval = null; +let trayIconState = 'normal'; // 'normal', 'update', 'blink' +let originalTrayIcon = null; +let updateTrayIcon = null; const UPDATE_CHECK_INTERVAL = 60 * 60 * 1000; // Check every hour +const GITHUB_REPO = 'whistlegraph/aesthetic-computer'; +const GITHUB_RELEASES_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`; try { autoUpdater = require('electron-updater').autoUpdater; @@ -136,6 +136,142 @@ setTimeout(checkForUpdates, 5000); // Then check every hour setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL); +} + +// GitHub release checking (works in dev mode too) +function checkGitHubForUpdates() { + const currentVersion = app.getVersion(); + console.log('[github] Checking for updates, current version:', currentVersion); + + const request = net.request(GITHUB_RELEASES_URL); + request.setHeader('User-Agent', 'Aesthetic-Computer-Electron'); + request.setHeader('Accept', 'application/vnd.github.v3+json'); + + let responseData = ''; + + request.on('response', (response) => { + response.on('data', (chunk) => { + responseData += chunk.toString(); + }); + + response.on('end', () => { + try { + const release = JSON.parse(responseData); + const latestVersion = release.tag_name?.replace(/^v/, '') || release.name?.replace(/^v/, ''); + + if (latestVersion && isNewerVersion(latestVersion, currentVersion)) { + console.log('[github] Update available:', latestVersion); + updateAvailable = { + version: latestVersion, + url: release.html_url, + releaseNotes: release.body, + publishedAt: release.published_at + }; + startTrayBlink(); + rebuildTrayMenu(); + } else { + console.log('[github] App is up to date'); + } + } catch (e) { + console.warn('[github] Failed to parse release info:', e.message); + } + }); + }); + + request.on('error', (err) => { + console.warn('[github] Failed to check for updates:', err.message); + }); + + request.end(); +} + +// Compare semantic versions +function isNewerVersion(latest, current) { + const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0); + const latestParts = parseVersion(latest); + const currentParts = parseVersion(current); + + for (let i = 0; i < 3; i++) { + const l = latestParts[i] || 0; + const c = currentParts[i] || 0; + if (l > c) return true; + if (l < c) return false; + } + return false; +} + +// Start blinking tray icon +function startTrayBlink() { + if (trayBlinkInterval || !tray) return; + + console.log('[tray] Starting update blink indicator'); + let blinkOn = true; + + trayBlinkInterval = setInterval(() => { + if (!tray) { + stopTrayBlink(); + return; + } + + if (blinkOn) { + // Show update indicator (colored dot or different icon) + if (updateTrayIcon) { + tray.setImage(updateTrayIcon); + } + // Also update title to show update available + if (process.platform === 'darwin') { + tray.setTitle('⬆️ Update'); + } + } else { + // Show normal icon + if (originalTrayIcon) { + tray.setImage(originalTrayIcon); + } + if (process.platform === 'darwin') { + updateTrayTitle(); + } + } + blinkOn = !blinkOn; + }, 1500); // Blink every 1.5 seconds +} + +// Stop blinking +function stopTrayBlink() { + if (trayBlinkInterval) { + clearInterval(trayBlinkInterval); + trayBlinkInterval = null; + } + if (tray && originalTrayIcon) { + tray.setImage(originalTrayIcon); + updateTrayTitle(); + } +} + +// Create update indicator icon (adds a colored badge) +function createUpdateIcon(baseIcon) { + if (process.platform === 'darwin') { + // On macOS, we can't easily modify template images, so we'll use title instead + return baseIcon; + } + + // For Windows/Linux, create a modified icon with a badge + try { + const size = baseIcon.getSize(); + const canvas = nativeImage.createEmpty(); + // For now, just return the base icon - could enhance with badge overlay later + return baseIcon; + } catch (e) { + return baseIcon; + } +} + +// Start GitHub update checks (works in both dev and production) +function startGitHubUpdateChecks() { + // Initial check after 10 seconds + setTimeout(checkGitHubForUpdates, 10000); + + // Then check every hour + setInterval(checkGitHubForUpdates, UPDATE_CHECK_INTERVAL); } // Set app name before anything else @@ -676,55 +812,16 @@ if (process.platform === 'darwin') { icon.setTemplateImage(true); } + // Store original icon for blink toggling + originalTrayIcon = icon; + updateTrayIcon = createUpdateIcon(icon); + tray = new Tray(icon); tray.setToolTip('Aesthetic Computer'); console.log('[main] System tray created successfully'); - // Build context menu - const contextMenu = Menu.buildFromTemplate([ - { - label: 'Show/Hide', - click: () => { - const allWindows = BrowserWindow.getAllWindows(); - if (allWindows.length > 0) { - const win = allWindows[0]; - if (win.isVisible()) { - allWindows.forEach(w => w.hide()); - } else { - allWindows.forEach(w => w.show()); - } - } else { - // No windows, open a new one - openDevWindow(); - } - } - }, - { type: 'separator' }, - { - label: 'New Window', - submenu: [ - { - label: 'Development (Flip View)', - click: () => openDevWindow() - }, - { - label: 'Production', - click: () => createWindow('production') - }, - { - label: 'Shell (Terminal)', - click: () => openShellWindow() - } - ] - }, - { type: 'separator' }, - { - label: 'Quit', - click: () => app.quit() - } - ]); - - tray.setContextMenu(contextMenu); + // Build and set the context menu + rebuildTrayMenu(); // On macOS, single click shows menu, on Windows/Linux it toggles window if (process.platform !== 'darwin') { @@ -747,6 +844,212 @@ // Set initial tray title updateTrayTitle(); } +// Rebuild the tray context menu (called when update becomes available) +function rebuildTrayMenu() { + if (!tray) return; + + const isMac = process.platform === 'darwin'; + const menuItems = []; + + // Update available section (if applicable) + if (updateAvailable) { + menuItems.push({ + label: `🆕 Update Available: v${updateAvailable.version}`, + click: () => { + shell.openExternal(updateAvailable.url); + stopTrayBlink(); + } + }); + menuItems.push({ + label: 'Download Update', + click: () => { + shell.openExternal(updateAvailable.url); + stopTrayBlink(); + } + }); + menuItems.push({ + label: 'Dismiss', + click: () => { + updateAvailable = null; + stopTrayBlink(); + rebuildTrayMenu(); + } + }); + menuItems.push({ type: 'separator' }); + } + + // File-like section + menuItems.push({ + label: 'Show/Hide', + accelerator: isMac ? 'Cmd+H' : 'Ctrl+H', + click: () => { + const allWindows = BrowserWindow.getAllWindows(); + if (allWindows.length > 0) { + const win = allWindows[0]; + if (win.isVisible()) { + allWindows.forEach(w => w.hide()); + } else { + allWindows.forEach(w => w.show()); + } + } else { + openDevWindow(); + } + } + }); + + menuItems.push({ type: 'separator' }); + + menuItems.push({ + label: 'New Window', + submenu: [ + { + label: 'Development (Flip View)', + accelerator: isMac ? 'Cmd+N' : 'Ctrl+N', + click: () => openDevWindow() + }, + { + label: 'Production', + accelerator: isMac ? 'Cmd+Shift+N' : 'Ctrl+Shift+N', + click: () => createWindow('production') + }, + { + label: 'Shell (Terminal)', + accelerator: isMac ? 'Cmd+T' : 'Ctrl+T', + click: () => openShellWindow() + } + ] + }); + + menuItems.push({ type: 'separator' }); + + // Edit section + menuItems.push({ + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + { role: 'selectAll' } + ] + }); + + // View section + menuItems.push({ + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }); + + // Navigate to pieces + menuItems.push({ + label: 'Navigate', + submenu: [ + { + label: 'Home (prompt)', + click: () => navigateToPiece('prompt') + }, + { + label: 'Starfield', + click: () => navigateToPiece('starfield') + }, + { + label: '1v1', + click: () => navigateToPiece('1v1') + }, + { type: 'separator' }, + { + label: 'Custom Piece...', + click: () => { + const win = getFocusedWindow(); + if (win) { + win.webContents.executeJavaScript(` + const piece = prompt('Enter piece name:'); + if (piece) window.location.href = window.location.origin + '/' + piece + '?nogap'; + `); + } + } + } + ] + }); + + menuItems.push({ type: 'separator' }); + + // Settings + menuItems.push({ + label: 'Preferences...', + accelerator: isMac ? 'Cmd+,' : 'Ctrl+,', + click: () => openPreferencesWindow() + }); + + menuItems.push({ type: 'separator' }); + + // Help section + menuItems.push({ + label: 'Help', + submenu: [ + { + label: 'Documentation', + click: () => shell.openExternal('https://aesthetic.computer/docs') + }, + { + label: 'GitHub Repository', + click: () => shell.openExternal('https://github.com/whistlegraph/aesthetic-computer') + }, + { + label: 'Check for Updates', + click: () => { + checkGitHubForUpdates(); + if (!updateAvailable) { + dialog.showMessageBox({ + type: 'info', + title: 'No Updates', + message: 'You\'re running the latest version!', + detail: `Current version: ${app.getVersion()}` + }); + } + } + }, + { type: 'separator' }, + { + label: `About Aesthetic Computer`, + click: () => { + dialog.showMessageBox({ + type: 'info', + title: 'About Aesthetic Computer', + message: 'Aesthetic Computer', + detail: `Version: ${app.getVersion()}\nElectron: ${process.versions.electron}\nChrome: ${process.versions.chrome}\nNode: ${process.versions.node}` + }); + } + } + ] + }); + + menuItems.push({ type: 'separator' }); + + menuItems.push({ + label: 'Quit', + accelerator: isMac ? 'Cmd+Q' : 'Alt+F4', + click: () => app.quit() + }); + + const contextMenu = Menu.buildFromTemplate(menuItems); + tray.setContextMenu(contextMenu); +} + +// ========== End System Tray ========== + // Update the tray title text (shown next to icon in menu bar) function updateTrayTitle(text) { if (!tray) return; @@ -793,6 +1096,35 @@ preferencesWindow.on('closed', () => { preferencesWindow = null; }); +} + +// Get the focused window or first available +function getFocusedWindow() { + let win = BrowserWindow.getFocusedWindow(); + if (!win) { + const allWindows = BrowserWindow.getAllWindows(); + win = allWindows.find(w => w.isVisible() && !w.isDestroyed()); + } + return win; +} + +// Navigate a window to a specific piece +function navigateToPiece(piece) { + const win = getFocusedWindow(); + if (win) { + win.webContents.executeJavaScript(` + window.location.href = window.location.origin + '/${piece}?nogap'; + `); + } else { + // No window open, create one and navigate + createWindow('development').then(result => { + result.window.webContents.once('did-finish-load', () => { + result.window.webContents.executeJavaScript(` + window.location.href = window.location.origin + '/${piece}?nogap'; + `); + }); + }); + } } // Open a new dev window - now uses 3D view @@ -1429,6 +1761,18 @@ }); ipcMain.handle('get-urls', () => URLS); +// CDP (Chrome DevTools Protocol) info +ipcMain.handle('get-cdp-info', () => { + const args = process.argv.join(' '); + const cdpMatch = args.match(/--remote-debugging-port=(\d+)/); + const inspectMatch = args.match(/--inspect=(\d+)/); + return { + enabled: !!cdpMatch, + port: cdpMatch ? cdpMatch[1] : null, + inspectPort: inspectMatch ? inspectMatch[1] : null + }; +}); + ipcMain.handle('check-docker', async () => { console.log('[main] check-docker called'); const result = await checkDocker(); @@ -1791,6 +2135,9 @@ app.whenReady().then(async () => { loadPreferences(); createMenu(); createSystemTray(); + + // Start GitHub update checks (works in dev and production) + startGitHubUpdateChecks(); // Check for updates on startup (production builds only) if (autoUpdater && !startInDevMode && app.isPackaged) { diff --git a/ac-electron/package-lock.json b/ac-electron/package-lock.json --- a/ac-electron/package-lock.json +++ b/ac-electron/package-lock.json @@ -18,7 +18,7 @@ "node-pty": "^1.1.0", "three": "^0.182.0" }, "devDependencies": { - "electron": "^33.4.11", + "electron": "^39.2.7", "electron-builder": "^25.1.8", "sharp": "^0.34.5" } @@ -1228,9 +1228,9 @@ "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", - "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3087,15 +3087,15 @@ "node": ">=0.10.0" } }, "node_modules/electron": { - "version": "33.4.11", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", - "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "version": "39.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", + "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", + "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { diff --git a/ac-electron/package.json b/ac-electron/package.json --- a/ac-electron/package.json +++ b/ac-electron/package.json @@ -127,7 +127,7 @@ "art", "graphics" ], "devDependencies": { - "electron": "^33.4.11", + "electron": "^39.2.7", "electron-builder": "^25.1.8", "sharp": "^0.34.5" }, diff --git a/ac-electron/renderer/development.html b/ac-electron/renderer/development.html --- a/ac-electron/renderer/development.html +++ b/ac-electron/renderer/development.html @@ -208,6 +208,33 @@ color: #f0f; } /* Flip button */ + /* CDP indicator */ + .cdp-indicator { + display: none; + align-items: center; + gap: 5px; + padding: 4px 8px; + background: rgba(0, 200, 255, 0.15); + border: 1px solid rgba(0, 200, 255, 0.4); + border-radius: 4px; + font-size: 10px; + color: #0cf; + } + .cdp-indicator.active { + display: flex; + } + .cdp-dot { + width: 6px; + height: 6px; + background: #0cf; + border-radius: 50%; + animation: cdpPulse 1.5s ease-in-out infinite; + } + @keyframes cdpPulse { + 0%, 100% { opacity: 0.4; box-shadow: 0 0 2px #0cf; } + 50% { opacity: 1; box-shadow: 0 0 8px #0cf; } + } + .flip-btn { background: rgba(255, 0, 255, 0.1); border: 1px solid rgba(255, 0, 255, 0.3); @@ -366,6 +393,12 @@
🩸 AC + +
+ + CDP +
+ @@ -423,6 +456,18 @@ const URLS = { local: 'https://localhost:8888', prod: 'https://aesthetic.computer' }; + + // Check for CDP (Chrome DevTools Protocol) on startup + (async () => { + const cdpInfo = await ipcRenderer.invoke('get-cdp-info'); + if (cdpInfo && cdpInfo.enabled) { + const indicator = document.getElementById('cdp-indicator'); + const portSpan = document.getElementById('cdp-port'); + indicator.classList.add('active'); + portSpan.textContent = `:${cdpInfo.port}`; + indicator.title = `DevTools: ws://127.0.0.1:${cdpInfo.port}`; + } + })(); let currentEnv = 'prod'; // 'local' or 'prod' - start with prod let currentPiece = 'starfield'; // default piece