honeytree 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/viewer.js DELETED
@@ -1,461 +0,0 @@
1
- import chalk from "chalk";
2
- import { scanCodebase } from "./scanner.js";
3
- import { generateForestCloud, generateGroundPlane } from "./pointcloud.js";
4
- import { createCamera, rotatePoint, projectPoint, clampElevation, clampAzimuth } from "./camera.js";
5
- import { createFrameBuffer, rasterize, renderBufferToString, renderTopBar, renderStatusBar } from "./renderer3d.js";
6
- import { getChangedFiles, getFileDiff, watchForChanges } from "./diffwatch.js";
7
- import { parseDiff } from "./diffparser.js";
8
- import { createDiffPanel, renderDiffPanel } from "./diffpanel.js";
9
- import { stageHunk, revertHunk } from "./diffactions.js";
10
-
11
- export function createForestWatcher(filePath, onChange) {
12
- try {
13
- const stat = fs.statSync(filePath);
14
- if (!stat.isFile()) return null;
15
-
16
- const watcher = fs.watch(filePath, onChange);
17
- watcher.on("error", () => {
18
- try { watcher.close(); } catch {}
19
- });
20
- return watcher;
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- function writeAnsi(code) {
27
- process.stdout.write(code);
28
- }
29
-
30
- function clearScreen() {
31
- writeAnsi("\x1b[2J\x1b[H");
32
- }
33
-
34
- function hideCursor() {
35
- writeAnsi("\x1b[?25l");
36
- }
37
-
38
- function showCursor() {
39
- writeAnsi("\x1b[?25h");
40
- }
41
-
42
- function moveHome() {
43
- writeAnsi("\x1b[H");
44
- }
45
-
46
- function enableMouse() {
47
- writeAnsi("\x1b[?1000h");
48
- writeAnsi("\x1b[?1002h");
49
- writeAnsi("\x1b[?1006h");
50
- }
51
-
52
- function disableMouse() {
53
- writeAnsi("\x1b[?1006l");
54
- writeAnsi("\x1b[?1002l");
55
- writeAnsi("\x1b[?1000l");
56
- }
57
-
58
- function enableMouseMotion() {
59
- writeAnsi("\x1b[?1003h");
60
- }
61
-
62
- function disableMouseMotion() {
63
- writeAnsi("\x1b[?1003l");
64
- }
65
-
66
- export async function viewer(targetDir) {
67
- const dir = targetDir || process.cwd();
68
-
69
- process.stdout.write("Scanning codebase...\n");
70
- const files = scanCodebase(dir);
71
-
72
- if (files.length === 0) {
73
- console.error("No source files found in", dir);
74
- process.exit(1);
75
- }
76
-
77
- const { points: treePoints, filePaths } = generateForestCloud(files);
78
- const groundRadius = Math.max(25, Math.sqrt(files.length) * 7);
79
- const groundPoints = generateGroundPlane(groundRadius);
80
- const allPoints = [...treePoints, ...groundPoints];
81
-
82
- const camera = createCamera();
83
- let mouseDown = false;
84
- let lastMouseX = 0;
85
- let lastMouseY = 0;
86
- let hoveredFile = "";
87
- let hoveredModified = false;
88
- let needsRedraw = true;
89
-
90
- let changedFiles = new Set();
91
- let diffPanel = null;
92
- let diffMode = false;
93
- let polling = false;
94
- let watcher = null;
95
-
96
- let screenWidth = process.stdout.columns || 80;
97
- let screenHeight = (process.stdout.rows || 24) - 2;
98
-
99
- function renderFrame() {
100
- screenWidth = process.stdout.columns || 80;
101
- screenHeight = (process.stdout.rows || 24) - 2;
102
-
103
- const forestWidth = diffPanel ? Math.floor(screenWidth * 0.4) : screenWidth;
104
- const buf = createFrameBuffer(forestWidth, screenHeight);
105
-
106
- const projected = [];
107
- for (const p of allPoints) {
108
- const [rx, ry, rz] = rotatePoint(p.x, p.y, p.z, camera.azimuth, camera.elevation);
109
- const proj = projectPoint(rx, ry, rz, forestWidth, screenHeight, camera.distance);
110
- if (proj.visible) {
111
- projected.push({
112
- ...proj,
113
- color: p.color,
114
- fileIndex: p.fileIndex,
115
- });
116
- }
117
- }
118
-
119
- rasterize(buf, projected);
120
-
121
- const changedIndices = diffMode
122
- ? new Set(files.map((f, i) => f.changed ? i : -1).filter(i => i >= 0))
123
- : null;
124
-
125
- moveHome();
126
- process.stdout.write(renderTopBar(hoveredFile, screenWidth, hoveredModified));
127
- process.stdout.write("\n");
128
-
129
- const forestLines = renderBufferToString(buf, undefined, changedIndices).split("\n");
130
-
131
- if (diffPanel) {
132
- const panelWidth = screenWidth - forestWidth - 1;
133
- const panelLines = renderDiffPanel(diffPanel, panelWidth, screenHeight);
134
- const border = chalk.hex("#555555")("│");
135
-
136
- for (let y = 0; y < screenHeight; y++) {
137
- const fLine = forestLines[y] || "";
138
- const pLine = panelLines[y] || "";
139
- process.stdout.write(fLine + border + pLine);
140
- if (y < screenHeight - 1) process.stdout.write("\n");
141
- }
142
- } else {
143
- process.stdout.write(forestLines.join("\n"));
144
- }
145
-
146
- process.stdout.write("\n");
147
-
148
- const changedCount = changedFiles.size;
149
- const changeText = changedCount > 0 ? `${changedCount} files changed | ` : "";
150
- process.stdout.write(renderStatusBar(files.length, screenWidth, changeText));
151
-
152
- return buf;
153
- }
154
-
155
- function regeneratePoints() {
156
- const { points: newTreePoints, filePaths: newPaths } = generateForestCloud(files);
157
- const newGround = generateGroundPlane(groundRadius);
158
- allPoints.length = 0;
159
- allPoints.push(...newTreePoints, ...newGround);
160
- filePaths.length = 0;
161
- filePaths.push(...newPaths);
162
- }
163
-
164
- async function pollChanges() {
165
- if (polling) return;
166
- polling = true;
167
- try {
168
- const newChanged = await getChangedFiles(dir);
169
- const changed = newChanged.size !== changedFiles.size ||
170
- [...newChanged].some(f => !changedFiles.has(f));
171
- changedFiles = newChanged;
172
-
173
- for (const file of files) {
174
- file.changed = changedFiles.has(file.relativePath);
175
- }
176
-
177
- if (changed) {
178
- regeneratePoints();
179
- needsRedraw = true;
180
- redraw();
181
- }
182
- } catch {
183
- // ignore poll errors
184
- }
185
- polling = false;
186
- }
187
-
188
- function checkAllResolved() {
189
- if (!diffPanel) return;
190
- const allDone = diffPanel.hunkStatus.every(s => s !== "pending");
191
- if (!allDone) return;
192
-
193
- for (let i = 0; i < diffPanel.hunks.length; i++) {
194
- if (diffPanel.hunkStatus[i] === "accepted") {
195
- stageHunk(dir, diffPanel.filePath, diffPanel.hunks[i]);
196
- } else if (diffPanel.hunkStatus[i] === "rejected") {
197
- revertHunk(dir, diffPanel.filePath, diffPanel.hunks[i]);
198
- }
199
- }
200
-
201
- diffPanel = null;
202
- clearScreen();
203
- pollChanges();
204
- }
205
-
206
- hideCursor();
207
- clearScreen();
208
- enableMouse();
209
- enableMouseMotion();
210
-
211
- // Render first frame before starting change detection
212
- let currentBuf = renderFrame();
213
- needsRedraw = false;
214
-
215
- // Start async change detection after first render
216
- pollChanges();
217
-
218
- // Watch filesystem for instant change detection
219
- watcher = watchForChanges(dir, () => pollChanges());
220
-
221
- // Fallback poll every 3 seconds in case fs.watch misses events
222
- const pollTimer = setInterval(() => pollChanges(), 3000);
223
-
224
- function redraw() {
225
- if (!needsRedraw) return;
226
- needsRedraw = false;
227
- try {
228
- currentBuf = renderFrame();
229
- } catch {
230
- // swallow render errors to prevent terminal corruption
231
- }
232
- }
233
-
234
- function parseMouseEvent(data) {
235
- const str = data.toString();
236
- const match = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
237
- if (!match) return null;
238
-
239
- const button = parseInt(match[1]);
240
- const x = parseInt(match[2]) - 1;
241
- const y = parseInt(match[3]) - 1;
242
- const released = match[4] === "m";
243
-
244
- return { button, x, y, released };
245
- }
246
-
247
- const cleanup = () => {
248
- clearInterval(pollTimer);
249
- if (watcher) watcher.close();
250
- disableMouseMotion();
251
- disableMouse();
252
- showCursor();
253
- clearScreen();
254
- console.log(`Scanned ${files.length} files in ${dir}`);
255
- process.exit(0);
256
- };
257
-
258
- process.on("SIGINT", cleanup);
259
- process.on("SIGTERM", cleanup);
260
- process.stdout.on("resize", () => {
261
- needsRedraw = true;
262
- clearScreen();
263
- redraw();
264
- });
265
-
266
- if (process.stdin.isTTY) {
267
- process.stdin.setRawMode(true);
268
- process.stdin.resume();
269
- process.stdin.on("data", (data) => {
270
- const key = data.toString();
271
-
272
- if (key === "\x03" || key === "q") {
273
- cleanup();
274
- return;
275
- }
276
-
277
- // Panel keybindings (when panel is open)
278
- if (diffPanel) {
279
- if (key === "\x1b" || key === "\x1b\x1b") {
280
- diffPanel = null;
281
- needsRedraw = true;
282
- clearScreen();
283
- redraw();
284
- return;
285
- }
286
- if (key === "j" || key === "\x1b[B") {
287
- diffPanel.currentHunk = Math.min(diffPanel.currentHunk + 1, diffPanel.hunks.length - 1);
288
- needsRedraw = true;
289
- redraw();
290
- return;
291
- }
292
- if (key === "k" || key === "\x1b[A") {
293
- diffPanel.currentHunk = Math.max(diffPanel.currentHunk - 1, 0);
294
- needsRedraw = true;
295
- redraw();
296
- return;
297
- }
298
- if (key === "a") {
299
- diffPanel.hunkStatus[diffPanel.currentHunk] = "accepted";
300
- const next = diffPanel.hunkStatus.findIndex((s, i) => i > diffPanel.currentHunk && s === "pending");
301
- if (next >= 0) diffPanel.currentHunk = next;
302
- checkAllResolved();
303
- needsRedraw = true;
304
- redraw();
305
- return;
306
- }
307
- if (key === "r") {
308
- diffPanel.hunkStatus[diffPanel.currentHunk] = "rejected";
309
- const next = diffPanel.hunkStatus.findIndex((s, i) => i > diffPanel.currentHunk && s === "pending");
310
- if (next >= 0) diffPanel.currentHunk = next;
311
- checkAllResolved();
312
- needsRedraw = true;
313
- redraw();
314
- return;
315
- }
316
- if (key === "A") {
317
- for (let i = 0; i < diffPanel.hunkStatus.length; i++) {
318
- if (diffPanel.hunkStatus[i] === "pending") diffPanel.hunkStatus[i] = "accepted";
319
- }
320
- checkAllResolved();
321
- needsRedraw = true;
322
- redraw();
323
- return;
324
- }
325
- if (key === "R") {
326
- for (let i = 0; i < diffPanel.hunkStatus.length; i++) {
327
- if (diffPanel.hunkStatus[i] === "pending") diffPanel.hunkStatus[i] = "rejected";
328
- }
329
- checkAllResolved();
330
- needsRedraw = true;
331
- redraw();
332
- return;
333
- }
334
- return;
335
- }
336
-
337
- if (key === "d") {
338
- diffMode = !diffMode;
339
- needsRedraw = true;
340
- redraw();
341
- return;
342
- }
343
-
344
- if (key === "r") {
345
- process.stdout.write("\x1b[H");
346
- process.stdout.write("Rescanning...");
347
- const newFiles = scanCodebase(dir);
348
- files.length = 0;
349
- files.push(...newFiles);
350
- regeneratePoints();
351
- needsRedraw = true;
352
- clearScreen();
353
- redraw();
354
- return;
355
- }
356
-
357
- if (key === "\x1b[D") {
358
- camera.azimuth = clampAzimuth(camera.azimuth - 5);
359
- needsRedraw = true;
360
- redraw();
361
- return;
362
- }
363
- if (key === "\x1b[C") {
364
- camera.azimuth = clampAzimuth(camera.azimuth + 5);
365
- needsRedraw = true;
366
- redraw();
367
- return;
368
- }
369
- if (key === "\x1b[A") {
370
- camera.elevation = clampElevation(camera.elevation + 5);
371
- needsRedraw = true;
372
- redraw();
373
- return;
374
- }
375
- if (key === "\x1b[B") {
376
- camera.elevation = clampElevation(camera.elevation - 5);
377
- needsRedraw = true;
378
- redraw();
379
- return;
380
- }
381
-
382
- if (key === "+" || key === "=") {
383
- camera.distance = Math.max(10, camera.distance - 5);
384
- needsRedraw = true;
385
- redraw();
386
- return;
387
- }
388
- if (key === "-" || key === "_") {
389
- camera.distance = Math.min(120, camera.distance + 5);
390
- needsRedraw = true;
391
- redraw();
392
- return;
393
- }
394
-
395
- const mouse = parseMouseEvent(data);
396
- if (!mouse) return;
397
-
398
- if (mouse.button === 0 && !mouse.released) {
399
- mouseDown = true;
400
- lastMouseX = mouse.x;
401
- lastMouseY = mouse.y;
402
- return;
403
- }
404
-
405
- if (mouse.released && mouse.button === 0) {
406
- const sy = mouse.y - 1;
407
- const sx = mouse.x;
408
- if (!diffPanel && currentBuf && sy >= 0 && sy < currentBuf.height && sx >= 0 && sx < currentBuf.width) {
409
- const fi = currentBuf.fileIndices[sy][sx];
410
- if (fi >= 0 && files[fi] && files[fi].changed) {
411
- const filePath = filePaths[fi];
412
- getFileDiff(dir, filePath).then((diff) => {
413
- if (diff) {
414
- const hunks = parseDiff(diff);
415
- if (hunks.length > 0) {
416
- diffPanel = createDiffPanel(filePath, hunks);
417
- needsRedraw = true;
418
- clearScreen();
419
- redraw();
420
- }
421
- }
422
- });
423
- mouseDown = false;
424
- return;
425
- }
426
- }
427
- mouseDown = false;
428
- return;
429
- }
430
-
431
- if (mouse.button === 32 && mouseDown) {
432
- const dx = mouse.x - lastMouseX;
433
- const dy = mouse.y - lastMouseY;
434
- lastMouseX = mouse.x;
435
- lastMouseY = mouse.y;
436
-
437
- camera.azimuth = clampAzimuth(camera.azimuth + dx * 0.8);
438
- camera.elevation = clampElevation(camera.elevation - dy * 0.8);
439
- needsRedraw = true;
440
- redraw();
441
- return;
442
- }
443
-
444
- if (mouse.button === 35 || (mouse.button >= 32 && mouse.released === false && !mouseDown)) {
445
- const sy = mouse.y - 1;
446
- const sx = mouse.x;
447
- if (currentBuf && sy >= 0 && sy < currentBuf.height && sx >= 0 && sx < currentBuf.width) {
448
- const fi = currentBuf.fileIndices[sy][sx];
449
- const newHover = fi >= 0 ? filePaths[fi] : "";
450
- const isModified = fi >= 0 && files[fi] && files[fi].changed;
451
- if (newHover !== hoveredFile) {
452
- hoveredFile = newHover;
453
- hoveredModified = isModified;
454
- writeAnsi(`\x1b[1;1H`);
455
- process.stdout.write(renderTopBar(hoveredFile, screenWidth, hoveredModified));
456
- }
457
- }
458
- }
459
- });
460
- }
461
- }