Monorepo for Aesthetic.Computer aesthetic.computer
at main 380 lines 9.7 kB view raw
1// 2D Canvas Process Tree Visualization 2// Dense, responsive, real-time process tree view 3 4(function() { 5 'use strict'; 6 7 const isVSCode = typeof acquireVsCodeApi === 'function'; 8 9 // Color schemes 10 const colorSchemes = { 11 dark: { 12 bg: '#181318', 13 fg: '#fff', 14 fgMuted: '#666', 15 accent: '#ff69b4', 16 online: '#0f0', 17 line: 'rgba(255, 255, 255, 0.15)', 18 nodeStroke: 'rgba(255, 255, 255, 0.3)', 19 categories: { 20 editor: '#b05070', 21 tui: '#ff5294', 22 bridge: '#6c757f', 23 db: '#ffb84b', 24 proxy: '#6bbd9f', 25 ai: '#ff95db', 26 shell: '#6c77ff', 27 dev: '#6c757f', 28 ide: '#6bbd9f', 29 lsp: '#889098', 30 kernel: '#889fff' 31 } 32 }, 33 light: { 34 bg: '#fcf7c5', 35 fg: '#281e5a', 36 fgMuted: '#806060', 37 accent: '#006400', 38 online: '#006400', 39 line: 'rgba(0, 0, 0, 0.15)', 40 nodeStroke: 'rgba(0, 0, 0, 0.3)', 41 categories: { 42 editor: '#804050', 43 tui: '#d02070', 44 bridge: '#202530', 45 db: '#a08020', 46 proxy: '#206050', 47 ai: '#c05090', 48 shell: '#004080', 49 dev: '#202530', 50 ide: '#206050', 51 lsp: '#606068', 52 kernel: '#387adf' 53 } 54 } 55 }; 56 57 let currentTheme = document.body.dataset.theme || 'dark'; 58 let scheme = colorSchemes[currentTheme]; 59 60 // Canvas setup 61 const canvas = document.getElementById('canvas'); 62 const ctx = canvas.getContext('2d'); 63 let width, height; 64 let dpr = window.devicePixelRatio || 1; 65 66 function resizeCanvas() { 67 width = canvas.clientWidth; 68 height = canvas.clientHeight; 69 canvas.width = width * dpr; 70 canvas.height = height * dpr; 71 ctx.scale(dpr, dpr); 72 } 73 resizeCanvas(); 74 window.addEventListener('resize', resizeCanvas); 75 76 // View transform (pan & zoom) 77 let offsetX = 0, offsetY = 0, scale = 1; 78 let isDragging = false, dragStartX = 0, dragStartY = 0; 79 80 // Process tree data 81 let processTree = []; 82 let nodeMap = new Map(); // pid -> node info 83 84 // Layout constants 85 const NODE_RADIUS = 6; 86 const NODE_SPACING_X = 180; 87 const NODE_SPACING_Y = 30; 88 const INDENT = 20; 89 90 // Build tree structure from flat process list 91 function buildTree(processes) { 92 if (!processes || !processes.length) return []; 93 94 const byPid = new Map(); 95 const children = new Map(); 96 97 processes.forEach(p => { 98 byPid.set(String(p.pid), p); 99 children.set(String(p.pid), []); 100 }); 101 102 let roots = []; 103 processes.forEach(p => { 104 const ppid = String(p.ppid); 105 if (ppid && byPid.has(ppid) && ppid !== String(p.pid)) { 106 children.get(ppid).push(p); 107 } else { 108 roots.push(p); 109 } 110 }); 111 112 // Layout tree with coordinates 113 nodeMap.clear(); 114 let yOffset = 20; 115 116 function layoutNode(proc, depth, parentY) { 117 const pid = String(proc.pid); 118 const x = depth * INDENT; 119 const y = yOffset; 120 yOffset += NODE_SPACING_Y; 121 122 const node = { 123 pid, 124 name: proc.name || proc.command || `PID ${pid}`, 125 cpu: proc.cpu || 0, 126 mem: proc.mem || 0, 127 category: proc.category || 'shell', 128 x, 129 y, 130 depth, 131 parentY, 132 children: [] 133 }; 134 135 nodeMap.set(pid, node); 136 137 const kids = children.get(pid) || []; 138 kids.forEach(child => { 139 const childNode = layoutNode(child, depth + 1, y); 140 if (childNode) node.children.push(childNode); 141 }); 142 143 return node; 144 } 145 146 return roots.map(r => layoutNode(r, 0, null)).filter(Boolean); 147 } 148 149 // Draw the tree 150 function draw() { 151 ctx.clearRect(0, 0, width, height); 152 153 ctx.save(); 154 ctx.translate(offsetX, offsetY); 155 ctx.scale(scale, scale); 156 157 // Draw connections first 158 nodeMap.forEach(node => { 159 if (node.parentY !== null) { 160 ctx.strokeStyle = scheme.line; 161 ctx.lineWidth = 1; 162 ctx.beginPath(); 163 ctx.moveTo(node.x, node.y); 164 ctx.lineTo(node.x - INDENT, node.parentY); 165 ctx.stroke(); 166 } 167 }); 168 169 // Draw nodes 170 nodeMap.forEach(node => { 171 const color = scheme.categories[node.category] || scheme.categories.shell; 172 173 // Node circle 174 ctx.fillStyle = color; 175 ctx.strokeStyle = scheme.nodeStroke; 176 ctx.lineWidth = 1.5; 177 ctx.beginPath(); 178 ctx.arc(node.x, node.y, NODE_RADIUS, 0, Math.PI * 2); 179 ctx.fill(); 180 ctx.stroke(); 181 182 // Node label 183 ctx.fillStyle = scheme.fg; 184 ctx.font = '11px monospace'; 185 ctx.textAlign = 'left'; 186 ctx.textBaseline = 'middle'; 187 ctx.fillText(node.name, node.x + NODE_RADIUS + 6, node.y); 188 189 // CPU/MEM info 190 if (node.cpu > 0.1 || node.mem > 0) { 191 ctx.fillStyle = scheme.fgMuted; 192 ctx.font = '9px monospace'; 193 const info = `${node.cpu.toFixed(1)}% • ${node.mem.toFixed(0)}MB`; 194 ctx.fillText(info, node.x + NODE_RADIUS + 6, node.y + 11); 195 } 196 }); 197 198 ctx.restore(); 199 } 200 201 // Mouse interaction 202 let hoveredNode = null; 203 204 function screenToWorld(screenX, screenY) { 205 return { 206 x: (screenX - offsetX) / scale, 207 y: (screenY - offsetY) / scale 208 }; 209 } 210 211 function findNodeAt(worldX, worldY) { 212 for (const [pid, node] of nodeMap) { 213 const dx = worldX - node.x; 214 const dy = worldY - node.y; 215 if (Math.sqrt(dx * dx + dy * dy) <= NODE_RADIUS + 2) { 216 return node; 217 } 218 } 219 return null; 220 } 221 222 canvas.addEventListener('mousedown', e => { 223 isDragging = true; 224 dragStartX = e.clientX - offsetX; 225 dragStartY = e.clientY - offsetY; 226 canvas.style.cursor = 'grabbing'; 227 }); 228 229 canvas.addEventListener('mousemove', e => { 230 const rect = canvas.getBoundingClientRect(); 231 const mouseX = e.clientX - rect.left; 232 const mouseY = e.clientY - rect.top; 233 234 if (isDragging) { 235 offsetX = e.clientX - dragStartX; 236 offsetY = e.clientY - dragStartY; 237 draw(); 238 } else { 239 const world = screenToWorld(mouseX, mouseY); 240 const node = findNodeAt(world.x, world.y); 241 242 if (node !== hoveredNode) { 243 hoveredNode = node; 244 if (node) { 245 showTooltip(e.clientX, e.clientY, node); 246 canvas.style.cursor = 'pointer'; 247 } else { 248 hideTooltip(); 249 canvas.style.cursor = 'default'; 250 } 251 } 252 } 253 }); 254 255 canvas.addEventListener('mouseup', () => { 256 isDragging = false; 257 canvas.style.cursor = 'default'; 258 }); 259 260 canvas.addEventListener('mouseleave', () => { 261 isDragging = false; 262 hideTooltip(); 263 canvas.style.cursor = 'default'; 264 }); 265 266 canvas.addEventListener('wheel', e => { 267 e.preventDefault(); 268 const delta = e.deltaY > 0 ? 0.9 : 1.1; 269 const newScale = Math.max(0.1, Math.min(5, scale * delta)); 270 271 const rect = canvas.getBoundingClientRect(); 272 const mouseX = e.clientX - rect.left; 273 const mouseY = e.clientY - rect.top; 274 275 offsetX = mouseX - (mouseX - offsetX) * (newScale / scale); 276 offsetY = mouseY - (mouseY - offsetY) * (newScale / scale); 277 scale = newScale; 278 279 draw(); 280 }); 281 282 // Tooltip 283 const tooltip = document.getElementById('tooltip'); 284 285 function showTooltip(x, y, node) { 286 tooltip.style.display = 'block'; 287 tooltip.style.left = (x + 10) + 'px'; 288 tooltip.style.top = (y + 10) + 'px'; 289 tooltip.textContent = `${node.name}\nPID: ${node.pid}\nCPU: ${node.cpu.toFixed(1)}%\nMem: ${node.mem.toFixed(0)} MB`; 290 } 291 292 function hideTooltip() { 293 tooltip.style.display = 'none'; 294 } 295 296 // Update from WebSocket data 297 function updateViz(processData) { 298 if (!processData?.interesting) return; 299 300 const processes = processData.interesting; 301 document.getElementById('process-count').textContent = `${processes.length} processes`; 302 303 processTree = buildTree(processes); 304 draw(); 305 } 306 307 // WebSocket connection 308 let ws; 309 function connectWS() { 310 const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; 311 const port = location.port || (protocol === 'wss:' ? '443' : '80'); 312 const wsUrl = `${protocol}//${location.hostname}:${port}/process-tree-ws`; 313 314 try { 315 ws = new WebSocket(wsUrl); 316 317 ws.onopen = () => { 318 console.log('Connected to process tree'); 319 document.getElementById('status-dot').classList.add('online'); 320 }; 321 322 ws.onclose = () => { 323 document.getElementById('status-dot').classList.remove('online'); 324 setTimeout(connectWS, 2000); 325 }; 326 327 ws.onmessage = (e) => { 328 try { 329 const data = JSON.parse(e.data); 330 if (data.system) { 331 document.getElementById('uptime').textContent = data.system.uptime?.formatted || '—'; 332 document.getElementById('cpu-info').textContent = `${data.system.cpus || 0} CPUs`; 333 const m = data.system.memory; 334 document.getElementById('mem-info').textContent = `${m?.used || 0} / ${m?.total || 0} MB`; 335 } 336 updateViz(data.processes); 337 } catch (err) { 338 console.error('Parse error:', err); 339 } 340 }; 341 } catch (err) { 342 console.error('WebSocket error:', err); 343 setTimeout(connectWS, 2000); 344 } 345 } 346 347 // Theme toggle 348 function toggleTheme() { 349 currentTheme = currentTheme === 'dark' ? 'light' : 'dark'; 350 scheme = colorSchemes[currentTheme]; 351 document.body.dataset.theme = currentTheme; 352 draw(); 353 } 354 355 // Reset view 356 function resetView() { 357 offsetX = 0; 358 offsetY = 0; 359 scale = 1; 360 draw(); 361 } 362 363 // Export API 364 window.ProcessTree2D = { 365 toggleTheme, 366 resetView, 367 updateViz 368 }; 369 370 // Start 371 connectWS(); 372 373 // Animation loop for smooth updates 374 function animate() { 375 // Could add animation logic here 376 requestAnimationFrame(animate); 377 } 378 animate(); 379 380})();