Monorepo for Aesthetic.Computer
aesthetic.computer
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})();