honeytree 1.1.6 → 1.2.0
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/LICENSE +21 -0
- package/README.md +43 -141
- package/bin/honeydew.js +4 -3
- package/package.json +9 -9
- package/src/animation-keyframes.js +890 -0
- package/src/animation.js +151 -0
- package/src/camera.js +54 -0
- package/src/diffactions.js +32 -0
- package/src/diffpanel.js +83 -0
- package/src/diffparser.js +44 -0
- package/src/diffwatch.js +52 -0
- package/src/pointcloud.js +193 -0
- package/src/renderer.js +49 -3
- package/src/renderer3d.js +135 -0
- package/src/scanner.js +102 -0
- package/src/sprites.js +2 -1
- package/src/viewer.js +375 -148
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
chalk.level = 3; // force 24-bit true color
|
|
4
|
+
|
|
5
|
+
const BLOCK_CHARS = ["█", "▓", "▒", "░"];
|
|
6
|
+
const BG_COLOR = "#0a0a1a";
|
|
7
|
+
|
|
8
|
+
function parseHex(hex) {
|
|
9
|
+
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
10
|
+
return {
|
|
11
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
12
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
13
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toHex({ r, g, b }) {
|
|
18
|
+
const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
|
|
19
|
+
return `#${c(r)}${c(g)}${c(b)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function lerpColor(hex, targetHex, factor) {
|
|
23
|
+
const c = parseHex(hex);
|
|
24
|
+
const t = parseHex(targetHex);
|
|
25
|
+
return toHex({
|
|
26
|
+
r: c.r + (t.r - c.r) * factor,
|
|
27
|
+
g: c.g + (t.g - c.g) * factor,
|
|
28
|
+
b: c.b + (t.b - c.b) * factor,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createFrameBuffer(width, height) {
|
|
33
|
+
const chars = Array.from({ length: height }, () =>
|
|
34
|
+
Array.from({ length: width }, () => ({ char: " ", color: null }))
|
|
35
|
+
);
|
|
36
|
+
const depth = Array.from({ length: height }, () =>
|
|
37
|
+
new Float64Array(width).fill(Infinity)
|
|
38
|
+
);
|
|
39
|
+
const fileIndices = Array.from({ length: height }, () =>
|
|
40
|
+
new Int32Array(width).fill(-1)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { chars, depth, fileIndices, width, height };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function rasterize(buf, projectedPoints, depthRange = null) {
|
|
47
|
+
let minDepth = Infinity;
|
|
48
|
+
let maxDepth = -Infinity;
|
|
49
|
+
|
|
50
|
+
if (depthRange) {
|
|
51
|
+
minDepth = depthRange.minDepth;
|
|
52
|
+
maxDepth = depthRange.maxDepth;
|
|
53
|
+
} else {
|
|
54
|
+
for (const p of projectedPoints) {
|
|
55
|
+
if (!p.visible) continue;
|
|
56
|
+
if (p.depth < minDepth) minDepth = p.depth;
|
|
57
|
+
if (p.depth > maxDepth) maxDepth = p.depth;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const depthSpan = maxDepth - minDepth || 1;
|
|
62
|
+
|
|
63
|
+
for (const p of projectedPoints) {
|
|
64
|
+
if (!p.visible) continue;
|
|
65
|
+
|
|
66
|
+
const sx = p.screenX;
|
|
67
|
+
const sy = p.screenY;
|
|
68
|
+
|
|
69
|
+
const t = (p.depth - minDepth) / depthSpan;
|
|
70
|
+
const charIndex = t === 0 ? 0 : Math.min(BLOCK_CHARS.length - 1, Math.ceil(t * BLOCK_CHARS.length) - 1);
|
|
71
|
+
const blockChar = BLOCK_CHARS[charIndex];
|
|
72
|
+
const dimFactor = t * 0.6;
|
|
73
|
+
const dimmedColor = lerpColor(p.color, BG_COLOR, dimFactor);
|
|
74
|
+
|
|
75
|
+
// Point splatting — closer points splat larger (2x2 for near, 1x1 for far)
|
|
76
|
+
const splatRadius = t < 0.3 ? 2 : t < 0.7 ? 1 : 0;
|
|
77
|
+
|
|
78
|
+
for (let dy = -splatRadius; dy <= splatRadius; dy++) {
|
|
79
|
+
for (let dx = -splatRadius; dx <= splatRadius; dx++) {
|
|
80
|
+
const px = sx + dx;
|
|
81
|
+
const py = sy + dy;
|
|
82
|
+
|
|
83
|
+
if (px < 0 || px >= buf.width || py < 0 || py >= buf.height) continue;
|
|
84
|
+
|
|
85
|
+
if (p.depth < buf.depth[py][px]) {
|
|
86
|
+
buf.depth[py][px] = p.depth;
|
|
87
|
+
buf.fileIndices[py][px] = p.fileIndex;
|
|
88
|
+
|
|
89
|
+
// Edge pixels of splat use lighter block char
|
|
90
|
+
const edgeChar = (dx === 0 && dy === 0) ? blockChar : BLOCK_CHARS[Math.min(BLOCK_CHARS.length - 1, charIndex + 1)];
|
|
91
|
+
buf.chars[py][px] = { char: edgeChar, color: dimmedColor };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renderBufferToString(buf, bgColor = BG_COLOR, changedIndices = null) {
|
|
99
|
+
const lines = [];
|
|
100
|
+
|
|
101
|
+
for (let y = 0; y < buf.height; y++) {
|
|
102
|
+
let line = "";
|
|
103
|
+
for (let x = 0; x < buf.width; x++) {
|
|
104
|
+
const cell = buf.chars[y][x];
|
|
105
|
+
if (!cell.color) {
|
|
106
|
+
line += chalk.hex(bgColor)(" ");
|
|
107
|
+
} else {
|
|
108
|
+
let color = cell.color;
|
|
109
|
+
if (changedIndices && buf.fileIndices[y][x] >= 0 && !changedIndices.has(buf.fileIndices[y][x])) {
|
|
110
|
+
color = lerpColor(color, bgColor, 0.6);
|
|
111
|
+
}
|
|
112
|
+
line += chalk.hex(color)(cell.char);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
lines.push(line);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderTopBar(hoveredFile, width, modified = false) {
|
|
122
|
+
if (!hoveredFile) return " ".repeat(width);
|
|
123
|
+
const tag = modified ? " [modified]" : "";
|
|
124
|
+
const plainLen = hoveredFile.length + tag.length + 2;
|
|
125
|
+
const pad = Math.max(0, Math.floor((width - plainLen) / 2));
|
|
126
|
+
const fileText = chalk.hex("#f5a50b")(` ${hoveredFile}`);
|
|
127
|
+
const tagText = modified ? chalk.hex("#ff8822")(" [modified]") : "";
|
|
128
|
+
return " ".repeat(pad) + fileText + tagText + " " + " ".repeat(Math.max(0, width - pad - plainLen));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function renderStatusBar(fileCount, width, prefix = "") {
|
|
132
|
+
const rightPart = `${prefix}${fileCount} files | drag to rotate | +/- zoom | d diff | q quit r rescan `;
|
|
133
|
+
const padding = Math.max(0, width - rightPart.length);
|
|
134
|
+
return " ".repeat(padding) + chalk.hex("#8e8a84")(rightPart);
|
|
135
|
+
}
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/scanner.js — walks project directory to collect file metadata
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const IGNORE_DIRS = new Set([
|
|
7
|
+
"node_modules", ".git", ".hg", ".svn", "dist", "build",
|
|
8
|
+
".next", "__pycache__", ".venv", "venv", "coverage",
|
|
9
|
+
".superpowers", ".honeytree",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const BINARY_EXTENSIONS = new Set([
|
|
13
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp",
|
|
14
|
+
".mp3", ".mp4", ".wav", ".avi", ".mov", ".mkv",
|
|
15
|
+
".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
|
|
16
|
+
".exe", ".dll", ".so", ".dylib", ".o", ".a",
|
|
17
|
+
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
18
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx",
|
|
19
|
+
".lock", ".sqlite", ".db",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function scanDirectory(rootDir) {
|
|
23
|
+
const files = [];
|
|
24
|
+
|
|
25
|
+
function walk(dir, relativeBase) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.name.startsWith(".") && entry.name !== ".") {
|
|
35
|
+
if (entry.isDirectory()) continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fullPath = path.join(dir, entry.name);
|
|
39
|
+
const relativePath = relativeBase
|
|
40
|
+
? `${relativeBase}/${entry.name}`
|
|
41
|
+
: entry.name;
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
45
|
+
walk(fullPath, relativePath);
|
|
46
|
+
} else if (entry.isFile()) {
|
|
47
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
48
|
+
if (BINARY_EXTENSIONS.has(ext)) continue;
|
|
49
|
+
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = fs.statSync(fullPath);
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
files.push({
|
|
58
|
+
absolutePath: fullPath,
|
|
59
|
+
relativePath,
|
|
60
|
+
extension: ext,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
directory: relativeBase || ".",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
walk(rootDir, "");
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getGitChurn(rootDir) {
|
|
73
|
+
try {
|
|
74
|
+
const output = execSync("git log --format= --name-only --diff-filter=ACMR", {
|
|
75
|
+
cwd: rootDir,
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
timeout: 10000,
|
|
78
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const counts = {};
|
|
82
|
+
for (const line of output.split("\n")) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed) continue;
|
|
85
|
+
counts[trimmed] = (counts[trimmed] || 0) + 1;
|
|
86
|
+
}
|
|
87
|
+
return counts;
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function scanCodebase(rootDir) {
|
|
94
|
+
const files = scanDirectory(rootDir);
|
|
95
|
+
const churn = getGitChurn(rootDir);
|
|
96
|
+
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
file.churn = churn[file.relativePath] || 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return files;
|
|
102
|
+
}
|
package/src/sprites.js
CHANGED
|
@@ -27,7 +27,7 @@ const DETAIL_COLORS = {
|
|
|
27
27
|
bushLight: "#5a8a4a",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
function parse(template, palette) {
|
|
30
|
+
export function parse(template, palette) {
|
|
31
31
|
const lines = template.trim().split("\n");
|
|
32
32
|
const width = Math.max(...lines.map((line) => line.length));
|
|
33
33
|
const rows = lines
|
|
@@ -300,3 +300,4 @@ export function getSprite(type, growth) {
|
|
|
300
300
|
}
|
|
301
301
|
return spriteSet[getGrowthStage(growth)];
|
|
302
302
|
}
|
|
303
|
+
|