From 6d26efa636299a7ae36c3480062467cbf9e04790 Mon Sep 17 00:00:00 2001 From: Jeffrey Alan Scudder Date: Tue, 13 Jan 2026 02:23:19 +0000 Subject: [PATCH] v0.1.11: Fix +/- window shortcuts, don't kill devcontainer on quit, simplify desktop.mjs --- ac-electron/main.js | 62 ++++++++++++++++++++++++++++++++++++++++++-------------------- ac-electron/package.json | 2 +- ac-electron/renderer/flip-view.html | 21 +++++++++++++++++++++ system/public/aesthetic.computer/disks/desktop.mjs | 175 +++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------ 4 file(s) changed, 95 insertion(s)(+), 165 deletion(s)(-) diff --git a/ac-electron/main.js b/ac-electron/main.js --- a/ac-electron/main.js +++ b/ac-electron/main.js @@ -2171,6 +2171,44 @@ } }); }); +// Handle webview window.open() calls (popups) from guest content +// This is required in Electron 12+ as new-window event is deprecated +app.on('web-contents-created', (event, contents) => { + // Only handle webviews + if (contents.getType() === 'webview') { + contents.setWindowOpenHandler(({ url }) => { + console.log('[main] Webview window.open:', url); + + // Handle ac://close request (from prompt.mjs '-' command) + if (url.startsWith('ac://close')) { + // Find the parent BrowserWindow of this webview + const allWindows = BrowserWindow.getAllWindows(); + for (const win of allWindows) { + if (!win.isDestroyed() && win.webContents.id === contents.hostWebContents?.id) { + win.close(); + break; + } + } + return { action: 'deny' }; + } + + // Handle new window request (from prompt.mjs '+' command) + // Open a new dev window and navigate to the URL + openDevWindow().then(({ window: newWin }) => { + if (newWin && !newWin.isDestroyed()) { + newWin.webContents.once('did-finish-load', () => { + if (!newWin.isDestroyed()) { + newWin.webContents.send('navigate', url); + } + }); + } + }); + + return { action: 'deny' }; // We handle it ourselves + }); + } +}); + app.on('window-all-closed', () => { // Quit when all windows are closed, even on macOS app.quit(); @@ -2186,19 +2224,8 @@ winData.ptyProcess.kill(); } } - // Stop the devcontainer when the app quits (only if docker is available) - try { - const dockerPath = getDockerPath(); - if (fs.existsSync(dockerPath)) { - require('child_process').execSync(`${dockerPath} stop aesthetic`, { - timeout: 5000, - stdio: 'ignore' // Suppress output to avoid EPIPE - }); - console.log('[main] Stopped devcontainer'); - } - } catch (e) { - // Silently ignore - docker may not be running or container may not exist - } + // NOTE: We intentionally do NOT stop the devcontainer when the app quits. + // The devcontainer should keep running for the VS Code workspace. }); // Handle certificate errors for localhost in dev mode @@ -2222,13 +2249,8 @@ winData.ptyProcess.kill(); } } - // Stop the devcontainer on cleanup - try { - require('child_process').execSync('docker stop aesthetic', { timeout: 5000 }); - console.log('[main] Stopped devcontainer'); - } catch (e) { - console.log('[main] Could not stop devcontainer:', e.message); - } + // NOTE: We intentionally do NOT stop the devcontainer. + // It should keep running for the VS Code workspace. process.exit(0); } diff --git a/ac-electron/package.json b/ac-electron/package.json --- a/ac-electron/package.json +++ b/ac-electron/package.json @@ -1,6 +1,6 @@ { "name": "aesthetic-computer", - "version": "0.1.10", + "version": "0.1.11", "description": "Aesthetic Computer - Creative coding platform", "homepage": "https://aesthetic.computer", "author": "Aesthetic Computer ", diff --git a/ac-electron/renderer/flip-view.html b/ac-electron/renderer/flip-view.html --- a/ac-electron/renderer/flip-view.html +++ b/ac-electron/renderer/flip-view.html @@ -817,6 +817,19 @@ ipcRenderer.invoke('set-current-piece', piece); } }); + // Handle window.open() from webview (prompt.mjs + and - commands) + // Note: Main process also handles this via setWindowOpenHandler + webviewEl.addEventListener('new-window', (e) => { + console.log('[flip] new-window event:', e?.url); + if (e?.preventDefault) e.preventDefault(); + const url = e?.url || ''; + if (url.startsWith('ac://close')) { + ipcRenderer.invoke('ac-close-window'); + return; + } + ipcRenderer.invoke('ac-open-window', { url }); + }); + // Mark this as the main window and set initial piece ipcRenderer.invoke('set-main-window'); ipcRenderer.invoke('set-current-piece', 'prompt'); @@ -909,6 +922,14 @@ // Listen for global flip shortcut from main process ipcRenderer.on('toggle-flip', () => { toggle(); + }); + + // Listen for navigate messages (from new window requests) + ipcRenderer.on('navigate', (event, url) => { + console.log('[flip] Navigate to:', url); + if (url && typeof url === 'string') { + webviewEl.src = url; + } }); // ========== Zoom Handling ========== diff --git a/system/public/aesthetic.computer/disks/desktop.mjs b/system/public/aesthetic.computer/disks/desktop.mjs --- a/system/public/aesthetic.computer/disks/desktop.mjs +++ b/system/public/aesthetic.computer/disks/desktop.mjs @@ -26,7 +26,7 @@ let electronApp = null; let isElectron = false; let updateStatus = null; // 'up-to-date', 'update-available', 'unknown' -let platform, downloadUrl, platformLabel, platformIcon; +let platform, downloadUrl, platformLabel; let buttons = []; let animFrame = 0; let hoverPlatform = null; @@ -135,19 +135,15 @@ if (plat.includes("mac") || ua.includes("mac")) { platform = "mac"; platformLabel = "macOS"; - platformIcon = "🍎"; } else if (plat.includes("win") || ua.includes("win")) { platform = "win"; platformLabel = "Windows"; - platformIcon = "🪟"; } else if (plat.includes("linux") || ua.includes("linux")) { platform = "linux"; platformLabel = "Linux"; - platformIcon = "🐧"; } else { platform = "mac"; platformLabel = "macOS"; - platformIcon = "🍎"; } } @@ -164,28 +160,27 @@ const isCompact = screen.height < 280; buttons = []; - const btnY = isCompact ? 100 : 130; - const btnGap = isMobile ? 6 : 12; + // Position buttons lower to avoid text overlap + const btnY = isCompact ? 110 : 150; + const btnGap = isMobile ? 8 : 14; const platforms = [ - { name: "mac", label: "Mac", icon: "🍎" }, - { name: "win", label: "Windows", icon: "🪟" }, - { name: "linux", label: "Linux", icon: "🐧" }, + { name: "mac", label: "Mac" }, + { name: "win", label: "Windows" }, + { name: "linux", label: "Linux" }, ]; // Calculate total width to center all buttons - const labels = platforms.map(p => `${p.icon} ${p.label}`); - const charWidth = 6; // TextButton default char width - const padding = 8; // TextButton internal padding (gap * 2) - const btnWidths = labels.map(l => l.length * charWidth + padding); + const charWidth = 6; + const padding = 12; + const btnWidths = platforms.map(p => p.label.length * charWidth + padding); const totalWidth = btnWidths.reduce((a, b) => a + b, 0) + btnGap * (platforms.length - 1); let x = Math.floor(screen.width / 2 - totalWidth / 2); platforms.forEach((p, i) => { - const label = `${p.icon} ${p.label}`; buttons.push({ ...p, - btn: new TB(label, { x, y: btnY }), + btn: new TB(p.label, { x, y: btnY }), isPrimary: p.name === platform, }); x += btnWidths[i] + btnGap; @@ -201,20 +196,11 @@ const bgPulse = wave(pulsePhase * 0.02) * 3; wipe(15 + bgPulse, 8 + bgPulse, 25 + bgPulse * 2); const cx = screen.width / 2; - const isMobile = screen.width < 400; const isCompact = screen.height < 280; - // Floating particles (subtle animation) - ink(255, 100, 255, 0.1); - for (let i = 0; i < 8; i++) { - const px = (animFrame * 0.3 + i * 120) % (screen.width + 40) - 20; - const py = (Math.sin((animFrame + i * 50) * 0.01) * 20 + 40 + i * 30) % screen.height; - box(px, py, 2, 2); - } - // Layout - let y = isCompact ? 16 : 28; - const spacing = isCompact ? 12 : 16; + let y = isCompact ? 20 : 32; + const spacing = isCompact ? 14 : 18; // Animated title with color cycling const titleHue = (animFrame * 0.5) % 360; @@ -227,65 +213,38 @@ // Subtitle with subtle pulse const subAlpha = 180 + wave(pulsePhase * 0.05) * 40; ink(200, 180, 220, subAlpha / 255).write("DESKTOP", { x: cx, y, center: "x" }); - y += spacing + 4; + y += spacing; // Version info - different display for Electron vs browser if (isElectron && electronApp) { - // Running inside Electron app - show current version + update status const currentVer = electronApp.version || "?"; const latestVer = releaseInfo.loaded ? releaseInfo.version : "..."; if (updateStatus === 'update-available') { - // Update available - highlight with animated color const urgePulse = wave(pulsePhase * 0.1) * 0.5 + 0.5; - ink(255 * urgePulse, 200, 100).write(`v${currentVer} → v${latestVer} available!`, { x: cx, y, center: "x" }); - y += 10; - ink(255, 180, 80).write("Download update below", { x: cx, y, center: "x" }); + ink(255 * urgePulse, 200, 100).write(`v${currentVer} -> v${latestVer} available`, { x: cx, y, center: "x" }); } else if (updateStatus === 'up-to-date') { - // Up to date - show green checkmark - ink(100, 200, 150).write(`✓ v${currentVer} (latest)`, { x: cx, y, center: "x" }); + ink(100, 200, 150).write(`v${currentVer} (latest)`, { x: cx, y, center: "x" }); } else { - // Unknown/loading ink(100, 80, 140).write(`v${currentVer}`, { x: cx, y, center: "x" }); - if (!releaseInfo.loaded) { - y += 10; - ink(70, 60, 100).write("checking for updates...", { x: cx, y, center: "x" }); - } } } else { - // Browser - show latest release version - const versionText = releaseInfo.loaded ? `v${releaseInfo.version}` : "loading..."; + const versionText = releaseInfo.loaded ? `v${releaseInfo.version}` : "..."; ink(100, 80, 140).write(versionText, { x: cx, y, center: "x" }); - - // Show release date if available - if (releaseInfo.publishedAt && !isCompact) { - y += 10; - const dateStr = releaseInfo.publishedAt.toLocaleDateString("en-US", { - year: "numeric", month: "short", day: "numeric" - }); - ink(70, 60, 100).write(dateStr, { x: cx, y, center: "x" }); - } } - y += spacing + 8; + y += spacing + 4; - // Platform detection with animated indicator + // Status line const detectPulse = wave(pulsePhase * 0.08) * 0.3 + 0.7; if (isElectron) { - // In app - show "Running in Desktop App" ink(100 * detectPulse, 200 * detectPulse, 255 * detectPulse); - write(`🖥️ Running in Desktop App`, { x: cx, y, center: "x" }); + write("Running in Desktop App", { x: cx, y, center: "x" }); } else { - // Browser - show platform detection ink(100 * detectPulse, 200 * detectPulse, 150 * detectPulse); - write(`${platformIcon} ${platformLabel} detected`, { x: cx, y, center: "x" }); + write(`${platformLabel} detected`, { x: cx, y, center: "x" }); } - y += spacing + 12; - // Download label - ink(140, 130, 160).write("Download for:", { x: cx, y, center: "x" }); - - // Platform buttons with proper color schemes - // TextButton scheme format: [fill, outline, textColor, textBg] + // Platform buttons buttons.forEach((b) => { const isPrimary = b.isPrimary; const isHover = hoverPlatform === b.name; @@ -294,109 +253,37 @@ let fillColor, outlineColor, textColor, textBg; if (isPrimary && !isHover) { - // Primary button: green glow fillColor = [60 + glow, 140 + glow, 100 + glow]; outlineColor = [100 + glow, 200, 150 + glow]; textColor = [220, 255, 240]; - textBg = fillColor; // Match fill + textBg = fillColor; } else if (isHover) { - // Hover: bright cyan fillColor = [40, 120, 160]; outlineColor = [80, 180, 220]; textColor = [200, 240, 255]; - textBg = fillColor; // Match fill + textBg = fillColor; } else { - // Normal: muted purple fillColor = [40, 30, 60]; outlineColor = [80, 60, 120]; textColor = [180, 160, 200]; - textBg = fillColor; // Match fill + textBg = fillColor; } b.btn.paint($, [fillColor, outlineColor, textColor, textBg], - [fillColor, outlineColor, textColor, textBg], // hover (handled above) - [[30, 25, 40], [50, 40, 70], [100, 90, 120], [30, 25, 40]] // disabled + [fillColor, outlineColor, textColor, textBg], + [[30, 25, 40], [50, 40, 70], [100, 90, 120], [30, 25, 40]] ); }); - // File type hint under buttons - const hintY = (isCompact ? 100 : 130) + 32; - ink(90, 80, 110); - const fileHint = hoverPlatform === "mac" ? ".dmg (Universal)" : - hoverPlatform === "win" ? ".exe (64-bit)" : - hoverPlatform === "linux" ? ".AppImage" : - platform === "mac" ? ".dmg (Universal)" : - platform === "win" ? ".exe (64-bit)" : ".AppImage"; - write(fileHint, { x: cx, y: hintY, center: "x" }); - - // Divider with gradient effect - const divY = (isCompact ? 100 : 130) + 54; - for (let dx = 0; dx < screen.width - 40; dx++) { - const alpha = Math.sin((dx / (screen.width - 40)) * Math.PI) * 0.5; - ink(100, 60, 140, alpha); - box(20 + dx, divY, 1, 1); - } - - // Features section - if (!isCompact && screen.height > 240) { - const featY = divY + 16; - - // Feature icons with staggered fade-in - const features = [ - { icon: "🌐", text: "Web + Terminal" }, - { icon: "🔍", text: "Zoom Controls" }, - { icon: "🖱️", text: "Drag & Scroll" }, - ]; - - const featSpacing = isMobile ? 90 : 120; - const featStart = cx - (features.length - 1) * featSpacing / 2; - - features.forEach((f, i) => { - const fadeIn = Math.min(1, (animFrame - i * 15) / 30); - if (fadeIn > 0) { - const fx = featStart + i * featSpacing; - ink(180, 150, 220, fadeIn); - write(f.icon, { x: fx, y: featY, center: "x" }); - ink(120, 100, 150, fadeIn * 0.8); - write(f.text, { x: fx, y: featY + 14, center: "x" }); - } - }); - } - - // Install instructions (context-sensitive) - if (!isCompact && screen.height > 280 && !isElectron) { - const instY = divY + (isMobile ? 50 : 56); - ink(80, 180, 120); - const instText = hoverPlatform === "mac" || (!hoverPlatform && platform === "mac") - ? "First run: Right-click → Open → Open" - : hoverPlatform === "linux" || (!hoverPlatform && platform === "linux") - ? "chmod +x *.AppImage && ./AC.AppImage" - : "Run installer and follow prompts"; - write(instText, { x: cx, y: instY, center: "x" }); - } - // Footer - const footY = screen.height - (isCompact ? 12 : 20); + const footY = screen.height - (isCompact ? 14 : 22); ink(60, 50, 80); - if (isElectron) { - // In app - show version details - const info = electronApp || {}; - write(`Electron ${info.electron || "?"} • Chrome ${info.chrome || "?"}`, { x: cx, y: footY, center: "x" }); + if (isElectron && electronApp) { + write(`Electron ${electronApp.electron || "?"}`, { x: cx, y: footY, center: "x" }); } else { - write("Requires Docker Desktop", { x: cx, y: footY, center: "x" }); + write("aesthetic.computer", { x: cx, y: footY, center: "x" }); } - - // Decorative corner accents - ink(100, 60, 140, 0.3); - line([0, 0], [20, 0]); - line([0, 0], [0, 20]); - line([screen.width - 1, 0], [screen.width - 21, 0]); - line([screen.width - 1, 0], [screen.width - 1, 20]); - line([0, screen.height - 1], [20, screen.height - 1]); - line([0, screen.height - 1], [0, screen.height - 21]); - line([screen.width - 1, screen.height - 1], [screen.width - 21, screen.height - 1]); - line([screen.width - 1, screen.height - 1], [screen.width - 1, screen.height - 21]); } // 🎪 Act -- tangled.sh