tri-cli 0.0.52 → 0.0.53
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/dist/Treemap.js +56 -20
- package/dist/cli.js +215 -26
- package/package.json +1 -1
package/dist/Treemap.js
CHANGED
|
@@ -20,6 +20,7 @@ const oneHunter = {
|
|
|
20
20
|
|
|
21
21
|
// Expanded color palette for directories
|
|
22
22
|
const dirColors = [oneHunter.blue, oneHunter.cyan, oneHunter.green, oneHunter.yellow, oneHunter.orange, oneHunter.red, oneHunter.purple, oneHunter.magenta, '#4a9eff', '#7ec699', '#e8b339', '#ff7eb6'];
|
|
23
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
24
|
|
|
24
25
|
// Function to format bytes to human readable
|
|
25
26
|
const formatBytes = bytes => {
|
|
@@ -30,6 +31,14 @@ const formatBytes = bytes => {
|
|
|
30
31
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
// Function to format counts
|
|
35
|
+
const formatCount = count => {
|
|
36
|
+
if (count === 0) return '0';
|
|
37
|
+
if (count < 1000) return count.toString();
|
|
38
|
+
if (count < 1000000) return (count / 1000).toFixed(1) + 'K';
|
|
39
|
+
return (count / 1000000).toFixed(1) + 'M';
|
|
40
|
+
};
|
|
41
|
+
|
|
33
42
|
// Get color based on file extension
|
|
34
43
|
const getFileColor = filename => {
|
|
35
44
|
var _filename$match;
|
|
@@ -99,8 +108,19 @@ const assignGroupColors = root => {
|
|
|
99
108
|
};
|
|
100
109
|
export const TreeMapView = ({
|
|
101
110
|
selectedNode,
|
|
102
|
-
stdout
|
|
111
|
+
stdout,
|
|
112
|
+
mode = 'size',
|
|
113
|
+
loadingPaths = new Set(),
|
|
114
|
+
calculatedCounts = new Map(),
|
|
115
|
+
calculatedSizes = new Map()
|
|
103
116
|
}) => {
|
|
117
|
+
const [spinnerFrame, setSpinnerFrame] = React.useState(0);
|
|
118
|
+
|
|
119
|
+
// Spinner animation loop
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
const interval = setInterval(() => setSpinnerFrame(f => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
122
|
+
return () => clearInterval(interval);
|
|
123
|
+
}, []);
|
|
104
124
|
if (!selectedNode || !selectedNode.data) {
|
|
105
125
|
return /*#__PURE__*/React.createElement(Box, {
|
|
106
126
|
flexDirection: "column"
|
|
@@ -110,16 +130,20 @@ export const TreeMapView = ({
|
|
|
110
130
|
}, "Error: Invalid node selected"), /*#__PURE__*/React.createElement(Text, {
|
|
111
131
|
dimColor: true,
|
|
112
132
|
color: oneHunter.textAlt
|
|
113
|
-
}, "Press
|
|
133
|
+
}, "Press any key to go back to tree view"));
|
|
114
134
|
}
|
|
115
135
|
const width = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.columns) || 80, 120) - 4;
|
|
116
136
|
const height = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.rows) || 24, 40) - 8;
|
|
117
137
|
|
|
138
|
+
// Choose formatter based on mode
|
|
139
|
+
const formatValue = mode === 'count' ? formatCount : formatBytes;
|
|
140
|
+
const modeLabel = mode === 'count' ? 'Count' : 'Size';
|
|
141
|
+
|
|
118
142
|
// If it's a file, show just that file as a big rectangle
|
|
119
143
|
if (selectedNode.data.isFile) {
|
|
120
144
|
const color = getFileColor(selectedNode.data.name);
|
|
121
145
|
const name = selectedNode.data.name;
|
|
122
|
-
const
|
|
146
|
+
const valueStr = mode === 'count' ? '1 file' : selectedNode.data.size ? formatBytes(selectedNode.data.size) : '';
|
|
123
147
|
const grid = Array(height).fill(null).map(() => Array(width).fill(null).map(() => ({
|
|
124
148
|
char: ' ',
|
|
125
149
|
color
|
|
@@ -149,12 +173,12 @@ export const TreeMapView = ({
|
|
|
149
173
|
};
|
|
150
174
|
}
|
|
151
175
|
}
|
|
152
|
-
if (
|
|
153
|
-
const sizeX = Math.floor((width -
|
|
154
|
-
for (let i = 0; i <
|
|
176
|
+
if (valueStr && midY + 2 < height - 2) {
|
|
177
|
+
const sizeX = Math.floor((width - valueStr.length) / 2);
|
|
178
|
+
for (let i = 0; i < valueStr.length && sizeX + i < width - 2; i++) {
|
|
155
179
|
if (sizeX + i > 0) {
|
|
156
180
|
grid[midY + 2][sizeX + i] = {
|
|
157
|
-
char:
|
|
181
|
+
char: valueStr[i],
|
|
158
182
|
color
|
|
159
183
|
};
|
|
160
184
|
}
|
|
@@ -194,10 +218,10 @@ export const TreeMapView = ({
|
|
|
194
218
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
195
219
|
bold: true,
|
|
196
220
|
color: oneHunter.cyan
|
|
197
|
-
}, "TreeMap View: ", name, " (File)"), /*#__PURE__*/React.createElement(Text, {
|
|
221
|
+
}, "TreeMap View (", modeLabel, "): ", name, " (File)"), /*#__PURE__*/React.createElement(Text, {
|
|
198
222
|
dimColor: true,
|
|
199
223
|
color: oneHunter.textAlt
|
|
200
|
-
}, "Press
|
|
224
|
+
}, "Press any key to go back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
|
|
201
225
|
borderStyle: "single",
|
|
202
226
|
borderColor: oneHunter.textAlt,
|
|
203
227
|
flexDirection: "column",
|
|
@@ -206,7 +230,7 @@ export const TreeMapView = ({
|
|
|
206
230
|
dimColor: true,
|
|
207
231
|
color: oneHunter.textAlt,
|
|
208
232
|
marginTop: 1
|
|
209
|
-
}, "File: ", name, " |
|
|
233
|
+
}, "File: ", name, " | ", modeLabel, ": ", valueStr || 'Unknown'));
|
|
210
234
|
}
|
|
211
235
|
|
|
212
236
|
// It's a directory - show treemap of its children
|
|
@@ -216,10 +240,10 @@ export const TreeMapView = ({
|
|
|
216
240
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
217
241
|
bold: true,
|
|
218
242
|
color: oneHunter.cyan
|
|
219
|
-
}, "TreeMap View: ", selectedNode.data.name, " (Empty Directory)"), /*#__PURE__*/React.createElement(Text, {
|
|
243
|
+
}, "TreeMap View (", modeLabel, "): ", selectedNode.data.name, " (Empty Directory)"), /*#__PURE__*/React.createElement(Text, {
|
|
220
244
|
dimColor: true,
|
|
221
245
|
color: oneHunter.textAlt
|
|
222
|
-
}, "Press
|
|
246
|
+
}, "Press any key to go back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
|
|
223
247
|
borderStyle: "single",
|
|
224
248
|
borderColor: oneHunter.textAlt,
|
|
225
249
|
marginTop: 1,
|
|
@@ -248,12 +272,18 @@ export const TreeMapView = ({
|
|
|
248
272
|
const x1 = Math.ceil(node.x1);
|
|
249
273
|
const y1 = Math.ceil(node.y1);
|
|
250
274
|
const isFile = node.data.isFile;
|
|
275
|
+
const isNodeLoading = loadingPaths.has(node.data.path);
|
|
251
276
|
const boxWidth = x1 - x0;
|
|
252
277
|
const boxHeight = y1 - y0;
|
|
253
278
|
if (boxWidth < 1 || boxHeight < 1) return;
|
|
254
279
|
|
|
255
|
-
// Color based on type
|
|
256
|
-
|
|
280
|
+
// Color based on type (dimmed if loading)
|
|
281
|
+
let color = isFile ? getFileColor(node.data.name) : node.groupColor || oneHunter.blue;
|
|
282
|
+
|
|
283
|
+
// Dim the color if loading
|
|
284
|
+
if (isNodeLoading) {
|
|
285
|
+
color = oneHunter.textAlt;
|
|
286
|
+
}
|
|
257
287
|
|
|
258
288
|
// Draw border
|
|
259
289
|
for (let y = y0; y < y1 && y < height; y++) {
|
|
@@ -277,14 +307,14 @@ export const TreeMapView = ({
|
|
|
277
307
|
|
|
278
308
|
// Draw label if there's room
|
|
279
309
|
const name = node.data.name;
|
|
280
|
-
const
|
|
310
|
+
const valueStr = isNodeLoading ? ` ${SPINNER_FRAMES[spinnerFrame]}` : node.value ? ` ${formatValue(node.value)}${mode === 'count' ? ' files' : ''}` : '';
|
|
281
311
|
if (boxWidth >= 8 && boxHeight >= 2) {
|
|
282
312
|
const labelY = y0 + 1;
|
|
283
313
|
const labelX = x0 + 1;
|
|
284
314
|
const maxLength = boxWidth - 2;
|
|
285
315
|
|
|
286
|
-
// Combine name and
|
|
287
|
-
let label = name +
|
|
316
|
+
// Combine name and value
|
|
317
|
+
let label = name + valueStr;
|
|
288
318
|
if (label.length > maxLength) {
|
|
289
319
|
// Try just name
|
|
290
320
|
if (name.length <= maxLength) {
|
|
@@ -374,15 +404,21 @@ export const TreeMapView = ({
|
|
|
374
404
|
key: y
|
|
375
405
|
}, segments);
|
|
376
406
|
});
|
|
407
|
+
const totalLabel = mode === 'count' ? `${formatCount(root.value || 0)} files` : formatBytes(root.value || 0);
|
|
408
|
+
const isLoading = loadingPaths.size > 0;
|
|
377
409
|
return /*#__PURE__*/React.createElement(Box, {
|
|
378
410
|
flexDirection: "column"
|
|
379
411
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
380
412
|
bold: true,
|
|
381
413
|
color: oneHunter.cyan
|
|
382
|
-
}, "TreeMap View: ", selectedNode.data.name), /*#__PURE__*/React.createElement(
|
|
414
|
+
}, "TreeMap View (", modeLabel, "): ", selectedNode.data.name), /*#__PURE__*/React.createElement(Box, {
|
|
415
|
+
justifyContent: "space-between"
|
|
416
|
+
}, /*#__PURE__*/React.createElement(Text, {
|
|
383
417
|
dimColor: true,
|
|
384
418
|
color: oneHunter.textAlt
|
|
385
|
-
}, "Press
|
|
419
|
+
}, "Press any key to go back to tree view | \u2191/\u2193: Navigate in tree | Ctrl+C: Exit"), isLoading && /*#__PURE__*/React.createElement(Text, {
|
|
420
|
+
color: oneHunter.yellow
|
|
421
|
+
}, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), /*#__PURE__*/React.createElement(Box, {
|
|
386
422
|
borderStyle: "single",
|
|
387
423
|
borderColor: oneHunter.textAlt,
|
|
388
424
|
flexDirection: "column",
|
|
@@ -391,5 +427,5 @@ export const TreeMapView = ({
|
|
|
391
427
|
dimColor: true,
|
|
392
428
|
color: oneHunter.textAlt,
|
|
393
429
|
marginTop: 1
|
|
394
|
-
}, "Showing ", firstLevelNodes.length, " items | Total:",
|
|
430
|
+
}, "Showing ", firstLevelNodes.length, " items | Total: ", totalLabel));
|
|
395
431
|
};
|
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,12 @@ const formatBytes = bytes => {
|
|
|
30
30
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
31
31
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
32
32
|
};
|
|
33
|
+
const formatCount = count => {
|
|
34
|
+
if (count === 0) return '0';
|
|
35
|
+
if (count < 1000) return count.toString();
|
|
36
|
+
if (count < 1000000) return (count / 1000).toFixed(1) + 'K';
|
|
37
|
+
return (count / 1000000).toFixed(1) + 'M';
|
|
38
|
+
};
|
|
33
39
|
const formatDate = timestamp => {
|
|
34
40
|
const date = new Date(timestamp);
|
|
35
41
|
const now = new Date();
|
|
@@ -66,6 +72,38 @@ const getDirSizeAsync = async dirPath => {
|
|
|
66
72
|
return 0;
|
|
67
73
|
}
|
|
68
74
|
};
|
|
75
|
+
|
|
76
|
+
// Fast file counting - much faster than size calculation
|
|
77
|
+
const countFilesAsync = async dirPath => {
|
|
78
|
+
try {
|
|
79
|
+
const stats = fs.statSync(dirPath);
|
|
80
|
+
if (!stats.isDirectory()) return 1;
|
|
81
|
+
let count = 0;
|
|
82
|
+
const countRecursive = async dir => {
|
|
83
|
+
try {
|
|
84
|
+
const entries = fs.readdirSync(dir, {
|
|
85
|
+
withFileTypes: true
|
|
86
|
+
});
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
// Skip hidden files
|
|
89
|
+
if (entry.name.startsWith('.')) continue;
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
if (entry.isFile()) {
|
|
92
|
+
count++;
|
|
93
|
+
} else if (entry.isDirectory()) {
|
|
94
|
+
await countRecursive(fullPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Skip directories we can't read
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
await countRecursive(dirPath);
|
|
102
|
+
return count;
|
|
103
|
+
} catch {
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
69
107
|
const readDirTree = (dirPath, noSize = true, currentLevel = 0, maxLevel = Infinity) => {
|
|
70
108
|
try {
|
|
71
109
|
const stats = fs.statSync(dirPath);
|
|
@@ -182,6 +220,7 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
182
220
|
shortcutInput,
|
|
183
221
|
noSize,
|
|
184
222
|
calculatedSizes,
|
|
223
|
+
calculatedCounts,
|
|
185
224
|
loadingPaths,
|
|
186
225
|
spinnerFrame,
|
|
187
226
|
lastModifiedDates
|
|
@@ -199,6 +238,7 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
199
238
|
} else {
|
|
200
239
|
displaySize = node.data.size;
|
|
201
240
|
}
|
|
241
|
+
const fileCount = calculatedCounts === null || calculatedCounts === void 0 ? void 0 : calculatedCounts.get(node.data.path);
|
|
202
242
|
const lastModified = lastModifiedDates.get(node.data.path);
|
|
203
243
|
const treeLines = getTreeLines(node);
|
|
204
244
|
|
|
@@ -235,7 +275,11 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
235
275
|
backgroundColor: isSelected ? oneHunter.green : undefined
|
|
236
276
|
}, node.data.name, !node.data.isFile && !isRoot ? '/' : ''), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
237
277
|
color: oneHunter.textAlt
|
|
238
|
-
}, " (", node.children.length, ")"),
|
|
278
|
+
}, " (", node.children.length, ")"), fileCount !== undefined && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, {
|
|
279
|
+
color: oneHunter.purple
|
|
280
|
+
}, " [", formatCount(fileCount), " files]"), isLoading && /*#__PURE__*/React.createElement(Text, {
|
|
281
|
+
color: oneHunter.yellow
|
|
282
|
+
}, " ", SPINNER_FRAMES[spinnerFrame])), isLoading ? /*#__PURE__*/React.createElement(Text, {
|
|
239
283
|
color: oneHunter.yellow
|
|
240
284
|
}, " ", SPINNER_FRAMES[spinnerFrame]) : displaySize > 0 ? /*#__PURE__*/React.createElement(Text, {
|
|
241
285
|
color: oneHunter.cyan
|
|
@@ -253,8 +297,10 @@ const TreeVisualization = ({
|
|
|
253
297
|
const [collapsed, setCollapsed] = useState(new Set());
|
|
254
298
|
const [shortcutInput, setShortcutInput] = useState('');
|
|
255
299
|
const [showTreeMap, setShowTreeMap] = useState(false);
|
|
300
|
+
const [treeMapMode, setTreeMapMode] = useState('size'); // 'size' or 'count'
|
|
256
301
|
const [initialized, setInitialized] = useState(false);
|
|
257
302
|
const [calculatedSizes, setCalculatedSizes] = useState(new Map());
|
|
303
|
+
const [calculatedCounts, setCalculatedCounts] = useState(new Map());
|
|
258
304
|
const [loadingPaths, setLoadingPaths] = useState(new Set());
|
|
259
305
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
260
306
|
const [rootTree, setRootTree] = useState(() => hierarchy(readDirTree(dirPath, noSize, 0, maxLevel)));
|
|
@@ -385,6 +431,85 @@ const TreeVisualization = ({
|
|
|
385
431
|
await new Promise(r => setTimeout(r, 0));
|
|
386
432
|
}
|
|
387
433
|
};
|
|
434
|
+
const countFilesInTreeAsync = async node => {
|
|
435
|
+
// Collect node and all its descendants
|
|
436
|
+
const nodesToCount = [];
|
|
437
|
+
const collectNodes = n => {
|
|
438
|
+
// Skip if already counted
|
|
439
|
+
if (calculatedCounts.has(n.data.path)) {
|
|
440
|
+
// Still recurse to children in case they need counting
|
|
441
|
+
if (n.children) {
|
|
442
|
+
n.children.forEach(collectNodes);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
nodesToCount.push(n);
|
|
447
|
+
if (n.children) {
|
|
448
|
+
n.children.forEach(collectNodes);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
collectNodes(node);
|
|
452
|
+
if (nodesToCount.length === 0) return; // Nothing to count
|
|
453
|
+
|
|
454
|
+
// Mark all nodes as loading
|
|
455
|
+
const pathsToLoad = nodesToCount.map(n => n.data.path);
|
|
456
|
+
setLoadingPaths(prev => new Set([...prev, ...pathsToLoad]));
|
|
457
|
+
const newMap = new Map(calculatedCounts);
|
|
458
|
+
|
|
459
|
+
// Count and stream updates for each node individually
|
|
460
|
+
for (const targetNode of nodesToCount) {
|
|
461
|
+
// For directories, stream partial counts as we count
|
|
462
|
+
if (!targetNode.data.isFile) {
|
|
463
|
+
let runningCount = 0;
|
|
464
|
+
let updateCounter = 0;
|
|
465
|
+
const countRecursive = async dir => {
|
|
466
|
+
try {
|
|
467
|
+
const entries = fs.readdirSync(dir, {
|
|
468
|
+
withFileTypes: true
|
|
469
|
+
});
|
|
470
|
+
for (const entry of entries) {
|
|
471
|
+
// Skip hidden files
|
|
472
|
+
if (entry.name.startsWith('.')) continue;
|
|
473
|
+
const fullPath = path.join(dir, entry.name);
|
|
474
|
+
if (entry.isFile()) {
|
|
475
|
+
runningCount++;
|
|
476
|
+
updateCounter++;
|
|
477
|
+
// Stream update every 100 files for better performance
|
|
478
|
+
if (updateCounter >= 100) {
|
|
479
|
+
updateCounter = 0;
|
|
480
|
+
newMap.set(targetNode.data.path, runningCount);
|
|
481
|
+
React.startTransition(() => {
|
|
482
|
+
setCalculatedCounts(new Map(newMap));
|
|
483
|
+
});
|
|
484
|
+
await new Promise(r => setTimeout(r, 0));
|
|
485
|
+
}
|
|
486
|
+
} else if (entry.isDirectory()) {
|
|
487
|
+
await countRecursive(fullPath);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// Skip directories we can't read
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
await countRecursive(targetNode.data.path);
|
|
495
|
+
newMap.set(targetNode.data.path, runningCount);
|
|
496
|
+
} else {
|
|
497
|
+
// Files are always 1
|
|
498
|
+
newMap.set(targetNode.data.path, 1);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Stream the final update and remove from loading
|
|
502
|
+
React.startTransition(() => {
|
|
503
|
+
setCalculatedCounts(new Map(newMap));
|
|
504
|
+
setLoadingPaths(prev => {
|
|
505
|
+
const next = new Set(prev);
|
|
506
|
+
next.delete(targetNode.data.path);
|
|
507
|
+
return next;
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
await new Promise(r => setTimeout(r, 0));
|
|
511
|
+
}
|
|
512
|
+
};
|
|
388
513
|
const getLastModifiedAsync = async node => {
|
|
389
514
|
// Collect node and all its descendants
|
|
390
515
|
const nodesToProcess = [];
|
|
@@ -460,8 +585,8 @@ const TreeVisualization = ({
|
|
|
460
585
|
return;
|
|
461
586
|
}
|
|
462
587
|
if (input === 'q') process.exit(0);
|
|
463
|
-
if (input === 'C') {
|
|
464
|
-
// Toggle collapse/expand all children of the selected node's PARENT
|
|
588
|
+
if (input === 'C' && !key.shift) {
|
|
589
|
+
// Toggle collapse/expand all children of the selected node's PARENT (lowercase handling moved)
|
|
465
590
|
const selectedNode = visibleNodes[selectedIndex];
|
|
466
591
|
if (!selectedNode || !selectedNode.parent) return;
|
|
467
592
|
const parentNode = selectedNode.parent;
|
|
@@ -492,6 +617,40 @@ const TreeVisualization = ({
|
|
|
492
617
|
});
|
|
493
618
|
return;
|
|
494
619
|
}
|
|
620
|
+
if (key.shift && input === 'C') {
|
|
621
|
+
// Shift+C: Show count treemap
|
|
622
|
+
const node = visibleNodes[selectedIndex];
|
|
623
|
+
if (!node) return;
|
|
624
|
+
|
|
625
|
+
// Collect all nodes that need counting
|
|
626
|
+
const nodesToCount = [];
|
|
627
|
+
const collectNodes = n => {
|
|
628
|
+
if (calculatedCounts.has(n.data.path)) {
|
|
629
|
+
if (n.children) {
|
|
630
|
+
n.children.forEach(collectNodes);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
nodesToCount.push(n);
|
|
635
|
+
if (n.children) {
|
|
636
|
+
n.children.forEach(collectNodes);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
collectNodes(node);
|
|
640
|
+
|
|
641
|
+
// Mark nodes as loading before starting
|
|
642
|
+
if (nodesToCount.length > 0) {
|
|
643
|
+
const pathsToLoad = nodesToCount.map(n => n.data.path);
|
|
644
|
+
setLoadingPaths(prev => new Set([...prev, ...pathsToLoad]));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Start calculating counts in the background
|
|
648
|
+
countFilesInTreeAsync(node);
|
|
649
|
+
// Show count treemap immediately
|
|
650
|
+
setTreeMapMode('count');
|
|
651
|
+
setShowTreeMap(true);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
495
654
|
if (input === 'O') {
|
|
496
655
|
// Toggle collapse/expand ALL nodes in the entire tree
|
|
497
656
|
const allNodesWithChildren = [];
|
|
@@ -524,7 +683,8 @@ const TreeVisualization = ({
|
|
|
524
683
|
if (!node) return;
|
|
525
684
|
// Always start calculating sizes in the background (calculateSizeAsync skips already-calculated nodes)
|
|
526
685
|
calculateSizeAsync(node);
|
|
527
|
-
// Show treemap immediately
|
|
686
|
+
// Show size treemap immediately
|
|
687
|
+
setTreeMapMode('size');
|
|
528
688
|
setShowTreeMap(true);
|
|
529
689
|
return;
|
|
530
690
|
}
|
|
@@ -533,6 +693,11 @@ const TreeVisualization = ({
|
|
|
533
693
|
if (node) await calculateSizeAsync(node);
|
|
534
694
|
return;
|
|
535
695
|
}
|
|
696
|
+
if (input === 'c') {
|
|
697
|
+
const node = visibleNodes[selectedIndex];
|
|
698
|
+
if (node) await countFilesInTreeAsync(node);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
536
701
|
if (input === 'l') {
|
|
537
702
|
const node = visibleNodes[selectedIndex];
|
|
538
703
|
if (node) await getLastModifiedAsync(node);
|
|
@@ -597,31 +762,52 @@ const TreeVisualization = ({
|
|
|
597
762
|
|
|
598
763
|
// Set value on the DATA object
|
|
599
764
|
const data = hierarchyNode.data;
|
|
600
|
-
if (
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
data.value =
|
|
605
|
-
} else {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
765
|
+
if (treeMapMode === 'count') {
|
|
766
|
+
// COUNT MODE: Use file counts
|
|
767
|
+
if (data.isFile) {
|
|
768
|
+
// Files always count as 1
|
|
769
|
+
data.value = 1;
|
|
770
|
+
} else if (data.children === undefined || !hierarchyNode.children || hierarchyNode.children.length === 0) {
|
|
771
|
+
// Directory with no children loaded OR empty directory
|
|
772
|
+
// Use calculatedCount if available
|
|
773
|
+
const calcCount = calculatedCounts.get(data.path);
|
|
774
|
+
if (calcCount !== undefined) {
|
|
775
|
+
data.value = calcCount;
|
|
776
|
+
} else {
|
|
609
777
|
data.value = 0;
|
|
610
778
|
}
|
|
611
|
-
}
|
|
612
|
-
} else if (data.children === undefined || !hierarchyNode.children || hierarchyNode.children.length === 0) {
|
|
613
|
-
// Directory with no children loaded OR empty directory
|
|
614
|
-
// Use calculatedSize if available
|
|
615
|
-
const calcSize = calculatedSizes.get(data.path);
|
|
616
|
-
if (calcSize !== undefined) {
|
|
617
|
-
data.value = calcSize;
|
|
618
779
|
} else {
|
|
619
|
-
//
|
|
620
|
-
data.value =
|
|
780
|
+
// Directory with loaded children - let d3's .sum() aggregate from children
|
|
781
|
+
data.value = undefined;
|
|
621
782
|
}
|
|
622
783
|
} else {
|
|
623
|
-
//
|
|
624
|
-
data.
|
|
784
|
+
// SIZE MODE: Use file sizes
|
|
785
|
+
if (data.isFile) {
|
|
786
|
+
// Files: use calculatedSize or read from disk
|
|
787
|
+
const calcSize = calculatedSizes.get(data.path);
|
|
788
|
+
if (calcSize !== undefined) {
|
|
789
|
+
data.value = calcSize;
|
|
790
|
+
} else {
|
|
791
|
+
try {
|
|
792
|
+
data.value = fs.statSync(data.path).size;
|
|
793
|
+
} catch {
|
|
794
|
+
data.value = 0;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} else if (data.children === undefined || !hierarchyNode.children || hierarchyNode.children.length === 0) {
|
|
798
|
+
// Directory with no children loaded OR empty directory
|
|
799
|
+
// Use calculatedSize if available
|
|
800
|
+
const calcSize = calculatedSizes.get(data.path);
|
|
801
|
+
if (calcSize !== undefined) {
|
|
802
|
+
data.value = calcSize;
|
|
803
|
+
} else {
|
|
804
|
+
// For unloaded directories without calculated size, will show as 0
|
|
805
|
+
data.value = data.size || 0;
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
// Directory with loaded children - let d3's .sum() aggregate from children
|
|
809
|
+
data.value = undefined;
|
|
810
|
+
}
|
|
625
811
|
}
|
|
626
812
|
};
|
|
627
813
|
populateValues(node);
|
|
@@ -629,7 +815,9 @@ const TreeVisualization = ({
|
|
|
629
815
|
selectedNode: node,
|
|
630
816
|
stdout: stdout,
|
|
631
817
|
calculatedSizes: calculatedSizes,
|
|
632
|
-
|
|
818
|
+
calculatedCounts: calculatedCounts,
|
|
819
|
+
loadingPaths: loadingPaths,
|
|
820
|
+
mode: treeMapMode
|
|
633
821
|
});
|
|
634
822
|
}
|
|
635
823
|
const start = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
|
|
@@ -643,7 +831,7 @@ const TreeVisualization = ({
|
|
|
643
831
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
644
832
|
color: oneHunter.textAlt,
|
|
645
833
|
dimColor: true
|
|
646
|
-
}, "\u2191/\u2193: Navigate | Enter: Expand/Traverse | d: Size | l: Date | m: Map | q: Quit"), loadingPaths.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
834
|
+
}, "\u2191/\u2193: Navigate | Enter: Expand/Traverse | c: Count | d: Size | l: Date | m: Map | Shift+C: CountMap | q: Quit"), loadingPaths.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
647
835
|
color: oneHunter.yellow
|
|
648
836
|
}, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
|
|
649
837
|
key: start + i,
|
|
@@ -653,6 +841,7 @@ const TreeVisualization = ({
|
|
|
653
841
|
shortcutInput: shortcutInput,
|
|
654
842
|
noSize: noSize,
|
|
655
843
|
calculatedSizes: calculatedSizes,
|
|
844
|
+
calculatedCounts: calculatedCounts,
|
|
656
845
|
loadingPaths: loadingPaths,
|
|
657
846
|
spinnerFrame: spinnerFrame,
|
|
658
847
|
lastModifiedDates: lastModifiedDates
|