Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
37 kB · 1171 lines
HTML
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-src https://localhost:* https://aesthetic.computer https://*.aesthetic.computer; connect-src 'self' https://localhost:* https://aesthetic.computer https://*.aesthetic.computer;"> <title>Aesthetic Computer</title> <link rel="stylesheet" href="../node_modules/@xterm/xterm/css/xterm.css"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: #050508; } /* Stacked layers - both always visible with opacity crossfade */ #device-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; background: #050508; -webkit-app-region: drag; } #flip-container { position: relative; width: 100%; height: 100%; -webkit-app-region: no-drag; } /* Both layers stacked - z-index swaps, opacity creates bleed-through */ #front-face, #back-face { position: absolute; width: 100%; height: 100%; overflow: hidden; transition: opacity 0.5s ease, z-index 0s; -webkit-app-region: no-drag; } /* Front face - AC App (default on top) */ #front-face { background: transparent; z-index: 2; opacity: 1; } /* Back face - Terminal (default behind with ghost) */ #back-face { background: transparent; display: flex; flex-direction: column; z-index: 1; opacity: 0.08; } /* Traffic lights area - keep space for window controls */ .traffic-lights-spacer { position: fixed; top: 0; left: 0; width: 80px; height: 40px; -webkit-app-region: drag; z-index: 1000; } /* Flip glow effect during animation */ @keyframes flipGlow { 0% { box-shadow: none; } 50% { box-shadow: 0 0 40px rgba(255, 0, 255, 0.4); } 100% { box-shadow: none; } } #flip-container.flipping #front-face, #flip-container.flipping #back-face { animation: flipGlow 0.8s ease-in-out; } /* Webview - inherits parent opacity */ #ac-webview { width: 100%; height: 100%; border: none; opacity: 0; transition: opacity 0.3s ease; } #ac-webview.connected { opacity: 1; } /* Terminal container - ensure transparency */ #terminal-container { flex: 1; padding: 15px; overflow: hidden; background: transparent; } .xterm { height: 100%; background: transparent !important; } .xterm-viewport { background: transparent !important; } .xterm-screen { background: transparent !important; } .xterm canvas { background: transparent !important; } /* When control bar is hidden */ body.bar-hidden #device-wrapper { top: 0; } /* Control bar - frameless window, positioned after traffic lights */ #control-bar { position: fixed; top: 8px; left: 80px; right: 15px; height: 32px; display: flex; align-items: center; gap: 12px; padding: 6px 12px; background: rgba(0, 0, 0, 0.8); border-radius: 8px; border: 1px solid rgba(255, 0, 255, 0.2); font-family: 'SF Mono', monospace; font-size: 11px; color: #888; z-index: 300; -webkit-app-region: no-drag; transition: transform 0.2s ease, opacity 0.2s ease; } #control-bar.hidden { transform: translateY(-100%); opacity: 0; pointer-events: none; } #control-bar > * { -webkit-app-region: no-drag; } .title { color: #f0f; cursor: default; -webkit-app-region: drag; } /* URL/Environment selector */ .env-switch { display: flex; background: rgba(0, 0, 0, 0.3); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1); } .env-btn { background: transparent; border: none; color: #666; padding: 5px 10px; font-family: inherit; font-size: 11px; cursor: pointer; transition: all 0.15s ease; } .env-btn:hover { color: #aaa; background: rgba(255, 255, 255, 0.05); } .env-btn.active.local { background: rgba(255, 150, 0, 0.2); color: #f90; } .env-btn.active.prod { background: rgba(0, 200, 100, 0.2); color: #0c6; } /* Mode switch - Front/Back flip */ .mode-switch { display: flex; background: rgba(0, 0, 0, 0.3); border-radius: 4px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1); } .mode-btn { background: transparent; border: none; color: #666; padding: 5px 12px; font-family: inherit; font-size: 11px; cursor: pointer; transition: all 0.15s ease; } .mode-btn:hover { color: #aaa; background: rgba(255, 255, 255, 0.05); } .mode-btn.active.front-mode { background: rgba(0, 255, 150, 0.2); color: #0fa; } .mode-btn.active.back-mode { background: rgba(255, 0, 255, 0.2); 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); color: #f0f; padding: 5px 12px; font-family: inherit; font-size: 11px; cursor: pointer; border-radius: 4px; transition: all 0.2s ease; } .flip-btn:hover { background: rgba(255, 0, 255, 0.2); transform: scale(1.05); } .flip-btn:active { transform: scale(0.95); } .flip-btn .flip-icon { display: inline-block; transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); } body.showing-back .flip-btn .flip-icon { transform: rotateY(180deg); } /* Transparency slider */ .slider-group { display: flex; align-items: center; gap: 8px; margin-left: auto; } .slider-label { color: #666; font-size: 11px; } #opacity-slider { -webkit-appearance: none; width: 100px; height: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; outline: none; } #opacity-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #f0f; border-radius: 50%; cursor: pointer; transition: transform 0.1s ease; } #opacity-slider::-webkit-slider-thumb:hover { transform: scale(1.2); } #opacity-value { color: #888; font-size: 11px; min-width: 35px; } .hint { color: #555; font-size: 11px; } .hint kbd { background: rgba(255, 255, 255, 0.1); padding: 2px 5px; border-radius: 3px; } /* Mode indicator overlay */ #mode-indicator { position: fixed; bottom: 40px; right: 40px; background: rgba(0, 0, 0, 0.9); padding: 12px 20px; border-radius: 8px; font-family: 'SF Mono', monospace; font-size: 16px; opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; z-index: 500; transform: translateY(10px); } #mode-indicator.front-active { color: #0fa; border: 1px solid rgba(0, 255, 150, 0.4); box-shadow: 0 0 20px rgba(0, 255, 150, 0.2); } #mode-indicator.back-active { color: #f0f; border: 1px solid rgba(255, 0, 255, 0.4); box-shadow: 0 0 20px rgba(255, 0, 255, 0.2); } #mode-indicator.visible { opacity: 1; transform: translateY(0); } /* Connection overlay (shown when server not ready) */ #connection-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #0a0a12; color: #888; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 14px; z-index: 50; transition: opacity 0.3s ease; } #connection-overlay.hidden { opacity: 0; pointer-events: none; } #status-icon { font-size: 48px; margin-bottom: 20px; animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } } #status-message { color: #f90; margin-bottom: 8px; } #status-detail { color: #666; font-size: 12px; margin-bottom: 20px; } #retry-count { color: #444; font-size: 11px; } </style></head><body> <!-- Traffic lights spacer for frameless window --> <div class="traffic-lights-spacer"></div> <!-- Connection overlay (for local dev server) --> <div id="connection-overlay" class="hidden"> <div id="status-icon">⚡</div> <div id="status-message">Connecting to dev server...</div> <div id="status-detail">https://localhost:8888</div> <div id="retry-count">Attempt 1</div> </div> <!-- Mode indicator (shows briefly on flip) --> <div id="mode-indicator">Back</div> <!-- Control bar --> <div id="control-bar"> <span class="title">🩸 AC</span> <!-- CDP indicator --> <div id="cdp-indicator" class="cdp-indicator"> <span class="cdp-dot"></span> <span>CDP <span id="cdp-port"></span></span> </div> <!-- Flip button --> <button id="flip-btn" class="flip-btn"><span class="flip-icon">↻</span> Flip</button> <!-- Environment switch (only shown on front) --> <div class="env-switch" id="env-switch"> <button id="env-local" class="env-btn local">Local</button> <button id="env-prod" class="env-btn prod active">Prod</button> </div> <span class="hint"><kbd>⌘`</kbd> flip · <kbd>⌘H</kbd> hide bar</span> </div> <!-- Device wrapper for 3D centering --> <div id="device-wrapper"> <!-- 3D Flip Container - the "device" --> <div id="flip-container"> <!-- Front Face - AC App --> <div id="front-face"> <webview id="ac-webview" src="about:blank" allowpopups></webview> </div> <!-- Back Face - Terminal --> <div id="back-face"> <div id="terminal-container"></div> </div> </div> </div>
<script> // With nodeIntegration, we can use require const { Terminal } = require('@xterm/xterm'); const { FitAddon } = require('@xterm/addon-fit'); const { WebglAddon } = require('@xterm/addon-webgl'); const { ipcRenderer } = require('electron');
// Elements const webview = document.getElementById('ac-webview'); const controlBar = document.getElementById('control-bar'); const flipContainer = document.getElementById('flip-container'); const frontFace = document.getElementById('front-face'); const backFace = document.getElementById('back-face'); const terminalContainer = document.getElementById('terminal-container'); const connectionOverlay = document.getElementById('connection-overlay'); const statusMessage = document.getElementById('status-message'); const statusDetail = document.getElementById('status-detail'); const retryCount = document.getElementById('retry-count'); const flipBtn = document.getElementById('flip-btn'); const envSwitch = document.getElementById('env-switch'); const envLocalBtn = document.getElementById('env-local'); const envProdBtn = document.getElementById('env-prod'); const modeIndicator = document.getElementById('mode-indicator');
// URLs 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 let attempts = 0; let serverConnected = false; let checkInterval = null; let showingBack = true; // Start showing back (terminal) let controlBarVisible = true; // Helper to build AC URL with electron-specific params function acUrl(piece) { const baseUrl = URLS[currentEnv]; return `${baseUrl}/${piece}?nogap`; }
// ========== Terminal Setup ========== const term = new Terminal({ cursorBlink: true, cursorStyle: 'block', fontSize: 13, fontFamily: "'SF Mono', 'Fira Code', monospace", scrollback: 5000, fastScrollModifier: 'alt', fastScrollSensitivity: 5, // Disable OSC query responses - prevents ]11;rgb:... from being sent back allowProposedApi: true, theme: { background: 'transparent', foreground: '#eee', cursor: '#f0f', cursorAccent: '#0a0a12', selectionBackground: 'rgba(255, 0, 255, 0.3)', black: '#1a1a2e', red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c', blue: '#6272a4', magenta: '#ff79c6', cyan: '#8be9fd', white: '#f8f8f2', }, allowTransparency: true, });
const fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(terminalContainer); // Load WebGL addon for GPU-accelerated rendering try { const webglAddon = new WebglAddon(); webglAddon.onContextLoss(() => { webglAddon.dispose(); }); term.loadAddon(webglAddon); console.log('[dev] WebGL renderer enabled'); } catch (e) { console.warn('[dev] WebGL not available, using canvas renderer'); } setTimeout(() => fitAddon.fit(), 50);
// Resize handling const resizeObserver = new ResizeObserver(() => { fitAddon.fit(); ipcRenderer.send('pty-resize', term.cols, term.rows); }); resizeObserver.observe(terminalContainer);
// ========== Environment Switching ========== function setEnv(env) { currentEnv = env; const baseUrl = URLS[env]; if (env === 'local') { envLocalBtn.classList.add('active'); envProdBtn.classList.remove('active'); statusDetail.textContent = baseUrl; // For local, check if server is running first if (!serverConnected) { connectionOverlay.classList.remove('hidden'); checkServer(); } else { webview.src = acUrl(currentPiece); } } else { envLocalBtn.classList.remove('active'); envProdBtn.classList.add('active'); // Production is always available connectionOverlay.classList.add('hidden'); webview.classList.add('connected'); webview.src = acUrl(currentPiece); if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } } updateTitle(); } envLocalBtn.addEventListener('click', () => setEnv('local')); envProdBtn.addEventListener('click', () => setEnv('prod')); // ========== Control Bar Visibility ========== function toggleControlBar() { controlBarVisible = !controlBarVisible; if (controlBarVisible) { controlBar.classList.remove('hidden'); document.body.classList.remove('bar-hidden'); } else { controlBar.classList.add('hidden'); document.body.classList.add('bar-hidden'); } // Refit terminal after layout change setTimeout(() => fitAddon.fit(), 50); }
// ========== Flip Animation ========== function flip() { showingBack = !showingBack; const frontFace = document.getElementById('front-face'); const backFace = document.getElementById('back-face'); if (showingBack) { // Show back (terminal) - swap z-index, crossfade opacity frontFace.style.zIndex = '1'; frontFace.style.opacity = '0.08'; backFace.style.zIndex = '2'; backFace.style.opacity = '1'; // Webview ghost, terminal full webview.style.opacity = '0.08'; webview.style.transition = 'opacity 0.5s ease'; terminalContainer.style.opacity = '1'; terminalContainer.style.transition = 'opacity 0.5s ease'; document.body.classList.add('showing-back'); modeIndicator.textContent = '🩸 Back'; modeIndicator.className = 'back-active visible'; envSwitch.style.display = 'none'; setTimeout(() => { term.focus(); fitAddon.fit(); }, 200); } else { // Show front (app) - swap z-index, crossfade opacity frontFace.style.zIndex = '2'; frontFace.style.opacity = '1'; backFace.style.zIndex = '1'; backFace.style.opacity = '0.08'; // Terminal ghost, webview full webview.style.opacity = '1'; webview.style.transition = 'opacity 0.5s ease'; terminalContainer.style.opacity = '0.08'; terminalContainer.style.transition = 'opacity 0.5s ease'; document.body.classList.remove('showing-back'); modeIndicator.textContent = '⚡ Front'; modeIndicator.className = 'front-active visible'; envSwitch.style.display = 'flex'; setTimeout(() => webview.focus(), 200); } // Hide indicator after a moment setTimeout(() => { modeIndicator.classList.remove('visible'); }, 1000); } // Initialize - start with back showing (terminal) flipContainer.classList.add('flipped'); document.body.classList.add('showing-back'); envSwitch.style.display = 'none'; // Set initial opacity states webview.style.opacity = '0.08'; terminalContainer.style.opacity = '1';
// Flip button click flipBtn.addEventListener('click', flip);
// Keyboard shortcuts document.addEventListener('keydown', async (e) => { // Cmd+C to copy selected text from terminal (when showing back) if ((e.metaKey || e.ctrlKey) && e.key === 'c' && showingBack) { if (term.hasSelection()) { e.preventDefault(); const selection = term.getSelection(); await navigator.clipboard.writeText(selection); } // If no selection, let Ctrl+C pass through to terminal (interrupt) } // Cmd+V to paste into terminal if ((e.metaKey || e.ctrlKey) && e.key === 'v' && showingBack) { e.preventDefault(); try { const text = await navigator.clipboard.readText(); if (text) { ipcRenderer.send('pty-input', text); } } catch (err) { // Clipboard access denied } } // Cmd+` to flip if ((e.metaKey || e.ctrlKey) && e.key === '`') { e.preventDefault(); flip(); } // Cmd+H to toggle control bar visibility if ((e.metaKey || e.ctrlKey) && e.key === 'h') { e.preventDefault(); toggleControlBar(); } // Cmd+1 for local, Cmd+2 for prod (only when showing front) if ((e.metaKey || e.ctrlKey) && e.key === '1' && !showingBack) { e.preventDefault(); setEnv('local'); } if ((e.metaKey || e.ctrlKey) && e.key === '2' && !showingBack) { e.preventDefault(); setEnv('prod'); } // Cmd+Shift+R to trigger full app reboot (electric-snake-bite) if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'R') { e.preventDefault(); term.writeln('\r\n\x1b[95m⚡🐍 Electric Snake Bite - Rebooting Electron...\x1b[0m\r\n'); await ipcRenderer.invoke('app-reboot'); } });
// ========== Server Connection ========== async function checkServer() { const baseUrl = URLS[currentEnv]; attempts++; retryCount.textContent = `Attempt ${attempts}`; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); await fetch(baseUrl, { method: 'HEAD', mode: 'no-cors', signal: controller.signal }); clearTimeout(timeout); statusMessage.textContent = 'Server found!'; serverConnected = true; webview.src = acUrl(currentPiece); return true; } catch (e) { return false; } }
// Webview load handlers webview.addEventListener('did-finish-load', async () => { const baseUrl = URLS[currentEnv]; if (webview.src.startsWith(baseUrl) || webview.src.startsWith(URLS.prod)) { serverConnected = true; statusMessage.textContent = 'Connected!'; // Inject Electron app info into webview for pieces like desktop.mjs try { const appInfo = await ipcRenderer.invoke('get-app-info'); await webview.executeJavaScript(`window.acElectronApp = ${JSON.stringify(appInfo)};`); } catch (e) { console.warn('Could not inject app info:', e); } setTimeout(() => { connectionOverlay.classList.add('hidden'); webview.classList.add('connected'); }, 300); if (checkInterval) { clearInterval(checkInterval); checkInterval = null; } } });
webview.addEventListener('did-fail-load', (e) => { if (e.errorCode === -3) return; webview.src = 'about:blank'; if (!checkInterval) { checkInterval = setInterval(checkServer, 2000); } });
// Title updates function updateTitle(url) { try { const u = new URL(url); currentPiece = u.pathname.replace(/^\//, '').replace(/\/$/, '') || 'prompt'; const envLabel = currentEnv === 'prod' ? '' : ' [DEV]'; document.title = `${currentPiece} — Aesthetic Computer${envLabel}`; } catch (e) { document.title = 'Aesthetic Computer'; } }
webview.addEventListener('did-navigate', (e) => updateTitle(e.url)); webview.addEventListener('did-navigate-in-page', (e) => updateTitle(e.url));
// Intercept window.open from inside the webview. // Used by AC prompt commands: '+' opens new window, '-' requests close via ac://close. webview.addEventListener('new-window', (e) => { 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 }); });
// Allow main process to navigate this window (used for newly spawned windows). ipcRenderer.on('navigate', (event, url) => { if (typeof url === 'string' && url.length) { webview.src = url; updateTitle(url); } });
// Menu handlers window.ac?.onNavigate?.((url) => { webview.src = url; updateTitle(url); }); window.ac?.onGoBack?.(() => webview.goBack()); window.ac?.onGoForward?.(() => webview.goForward()); window.ac?.onToggleDevtools?.(() => { if (webview.isDevToolsOpened()) webview.closeDevTools(); else webview.openDevTools(); });
// ========== PTY Connection ========== async function initShell() { try { const dockerOk = await ipcRenderer.invoke('check-docker'); if (!dockerOk) { term.writeln('\x1b[31mError: Docker not running\x1b[0m'); return; }
const containerRunning = await ipcRenderer.invoke('check-container'); if (!containerRunning) { // Check if container exists but is stopped const containerExists = await ipcRenderer.invoke('check-container-exists'); if (containerExists) { term.writeln('\x1b[33mStarting existing container...\x1b[0m'); await ipcRenderer.invoke('start-existing-container'); } else { term.writeln('\x1b[33mCreating devcontainer...\x1b[0m'); await ipcRenderer.invoke('start-container'); } }
const connected = await ipcRenderer.invoke('connect-pty'); if (!connected) { term.writeln('\x1b[31mFailed to connect to shell\x1b[0m'); return; }
// ⚡🐍 Electric Snake Bite - Special escape sequence detection // When the devcontainer outputs this magic string, reload the window const ELECTRIC_SNAKE_BITE = '\x1b]9999;electric-snake-bite\x07'; let escapeBuffer = ''; let dataCount = 0; ipcRenderer.on('pty-data', (event, data) => { dataCount++; // Check for electric-snake-bite escape sequence (must be the FULL OSC sequence) escapeBuffer += data; if (escapeBuffer.length > 100) { escapeBuffer = escapeBuffer.slice(-50); // Keep buffer small } // Only trigger on the actual OSC 9999 escape sequence, not plain text if (escapeBuffer.includes('\x1b]9999;electric-snake-bite')) { escapeBuffer = ''; // Don't write the escape sequence to terminal const cleanData = data.replace(/\x1b\]9999;electric-snake-bite\x07/g, ''); if (cleanData) term.write(cleanData); // Reload after a brief delay setTimeout(() => location.reload(), 100); return; } term.write(data); }); // Filter out terminal query responses before sending to PTY // These are responses to OSC queries like ]11;rgb:... (background color) // Buffer small inputs to catch responses that come character by character let inputBuffer = ''; let inputTimeout = null; term.onData((data) => { // Accumulate data inputBuffer += data; // Clear any pending flush if (inputTimeout) clearTimeout(inputTimeout); // Flush after short delay or when buffer gets large enough inputTimeout = setTimeout(() => { const toSend = inputBuffer; inputBuffer = ''; // Filter out OSC responses // Check for escape sequence form if (toSend.match(/\x1b\]1[0-9];/) || toSend.match(/\x1b\[\?/)) { return; } // Filter plain OSC responses without escape if (toSend.match(/^\]1[0-9];/) || toSend.match(/\]1[0-9];rgb:/)) { return; } // Filter responses that contain rgb: color data or OSC markers if (toSend.includes(';rgb:') || toSend.includes(']11;') || toSend.includes(']10;')) { return; } ipcRenderer.send('pty-input', toSend); }, 5); // 5ms buffer - enough to catch multi-char sequences }); ipcRenderer.on('pty-exit', (event, code) => { term.writeln(`\r\n\x1b[33mShell exited with code ${code}\x1b[0m`); });
ipcRenderer.send('pty-resize', term.cols, term.rows); // Auto-start emacs with proper daemon wait // The container may need time for emacs daemon to fully initialize setTimeout(() => { // First ensure emacs daemon is ready, then connect // Use a retry-aware startup command ipcRenderer.send('pty-input', 'ensure-emacs-daemon-ready --timeout=60 && ac-aesthetic\n'); // After emacs loads, send resize and refresh to sync terminal setTimeout(() => { // Fit terminal first fitAddon.fit(); // Send resize to PTY ipcRenderer.send('pty-resize', term.cols, term.rows); // Redraw emacs ipcRenderer.send('pty-input', '\x0c'); // Ctrl+L to redraw emacs }, 3000); // Wait longer for daemon init }, 500); // Give fish a moment to fully init
} catch (err) { term.writeln(`\x1b[31mError: ${err.message}\x1b[0m`); } }
// ========== IPC Bridge for Artery/Tests ========== // These handlers allow artery-tui to control the webview for testing // Navigate to a piece ipcRenderer.on('ac-navigate', (event, piece) => { const baseUrl = URLS[currentEnv]; currentPiece = piece; webview.src = acUrl(piece); updateTitle(webview.src); }); // Switch environment ipcRenderer.on('ac-set-env', (event, env) => { if (env === 'local' || env === 'prod') { setEnv(env); } }); // Execute JavaScript in the webview ipcRenderer.on('ac-eval', async (event, code) => { try { const result = await webview.executeJavaScript(code); ipcRenderer.send('ac-eval-result', { success: true, result }); } catch (e) { ipcRenderer.send('ac-eval-result', { success: false, error: e.message }); } }); // Get current state ipcRenderer.on('ac-get-state', (event) => { ipcRenderer.send('ac-state', { env: currentEnv, piece: currentPiece, url: webview.src, mode: currentMode, connected: serverConnected }); }); // Toggle shell/app mode ipcRenderer.on('ac-set-mode', (event, mode) => { if (mode === 'shell' || mode === 'app') { setMode(mode); } }); // Take screenshot of webview ipcRenderer.on('ac-screenshot', async (event) => { try { const nativeImage = await webview.capturePage(); const dataUrl = nativeImage.toDataURL(); ipcRenderer.send('ac-screenshot-result', { success: true, dataUrl }); } catch (e) { ipcRenderer.send('ac-screenshot-result', { success: false, error: e.message }); } }); // Reload webview ipcRenderer.on('ac-reload', (event) => { webview.reload(); });
// ========== File-Based Bridge for Devcontainer Tests ========== // Watches for command files from artery-electron.mjs in the devcontainer const { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } = require('fs'); const pathModule = require('path'); // @electron/remote is optional - don't crash if not available let remote = null; try { remote = require('@electron/remote'); } catch (e) { } // Get workspace root - the parent of ac-electron // In dev mode, process.cwd() is usually the workspace root // Or we can use the app path let WORKSPACE_ROOT = process.cwd(); // Verify we found the right place if (!existsSync(pathModule.join(WORKSPACE_ROOT, '.electron-bridge')) && !existsSync(pathModule.join(WORKSPACE_ROOT, 'ac-electron'))) { // Try going up from current directory let testPath = WORKSPACE_ROOT; for (let i = 0; i < 5; i++) { if (existsSync(pathModule.join(testPath, 'ac-electron'))) { WORKSPACE_ROOT = testPath; break; } testPath = pathModule.dirname(testPath); } } const BRIDGE_DIR = pathModule.join(WORKSPACE_ROOT, '.electron-bridge'); const CMD_FILE = pathModule.join(BRIDGE_DIR, 'command.json'); const RESULT_FILE = pathModule.join(BRIDGE_DIR, 'result.json'); let lastProcessedId = null; let bridgeWatcher = null; async function processCommand(request) { const { id, cmd, params } = request; // Skip if already processed if (id === lastProcessedId) return; lastProcessedId = id; let result = { id, data: null, error: null }; try { switch (cmd) { case 'get-state': result.data = { env: currentEnv, piece: currentPiece, url: webview.src, mode: currentMode, connected: serverConnected }; break; case 'navigate': const baseUrl = URLS[currentEnv]; currentPiece = params.piece; webview.src = acUrl(params.piece); updateTitle(webview.src); result.data = { success: true, piece: params.piece }; break; case 'eval': try { const evalResult = await webview.executeJavaScript(params.code); result.data = { success: true, result: evalResult }; } catch (e) { result.data = { success: false, error: e.message }; } break; case 'reload': webview.reload(); result.data = { success: true }; break; case 'screenshot': try { const nativeImage = await webview.capturePage(); result.data = { success: true, dataUrl: nativeImage.toDataURL() }; } catch (e) { result.data = { success: false, error: e.message }; } break; case 'set-env': if (params.env === 'local' || params.env === 'prod') { setEnv(params.env); result.data = { success: true, env: params.env }; } else { result.error = 'Invalid env'; } break; case 'set-mode': if (params.mode === 'shell' || params.mode === 'app') { setMode(params.mode); result.data = { success: true, mode: params.mode }; } else { result.error = 'Invalid mode'; } break; case 'enable-console': // Console capture - could forward to a log file result.data = { success: true }; break; default: result.error = `Unknown command: ${cmd}`; } } catch (e) { result.error = e.message; } // Write result writeFileSync(RESULT_FILE, JSON.stringify(result, null, 2)); } function checkBridgeCommand() { try { if (existsSync(CMD_FILE)) { const content = readFileSync(CMD_FILE, 'utf8'); const request = JSON.parse(content); if (request.id !== lastProcessedId) { processCommand(request); } } } catch (e) { // Ignore read errors } } function initBridge() { try { // Ensure bridge directory exists if (!existsSync(BRIDGE_DIR)) { mkdirSync(BRIDGE_DIR, { recursive: true }); } // Poll for commands (file watching can be unreliable across Docker) setInterval(checkBridgeCommand, 100); console.log('[dev] File bridge initialized at', BRIDGE_DIR); } catch (e) { console.warn('[dev] Could not init file bridge:', e.message); } }
// ========== Init ========== setEnv('prod'); // Start with production - loads /starfield initShell(); initBridge(); // Ensure webview loads immediately setTimeout(() => { if (webview.src === 'about:blank') { webview.src = acUrl('starfield'); } }, 100); // ========== Alt + Scroll to Drag Window ========== document.addEventListener('wheel', (e) => { // Alt + scroll (two-finger drag with alt) moves the window if (e.altKey) { e.preventDefault(); const currentX = window.screenX; const currentY = window.screenY; const newX = currentX - e.deltaX; const newY = currentY - e.deltaY; ipcRenderer.send('move-window', { x: Math.round(newX), y: Math.round(newY) }); } }, { passive: false }); </script></body></html>