tri-cli 0.0.51 → 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 +62 -24
- package/dist/cli.js +219 -29
- package/package.json +15 -5
- package/readme.tape +19 -32
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,9 +31,18 @@ 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;
|
|
45
|
+
const ext = (_filename$match = filename.match(/\.[^.]+$/)) === null || _filename$match === void 0 || (_filename$match = _filename$match[0]) === null || _filename$match === void 0 ? void 0 : _filename$match.toLowerCase();
|
|
36
46
|
const colorMap = {
|
|
37
47
|
'.js': oneHunter.yellow,
|
|
38
48
|
'.jsx': oneHunter.yellow,
|
|
@@ -98,8 +108,19 @@ const assignGroupColors = root => {
|
|
|
98
108
|
};
|
|
99
109
|
export const TreeMapView = ({
|
|
100
110
|
selectedNode,
|
|
101
|
-
stdout
|
|
111
|
+
stdout,
|
|
112
|
+
mode = 'size',
|
|
113
|
+
loadingPaths = new Set(),
|
|
114
|
+
calculatedCounts = new Map(),
|
|
115
|
+
calculatedSizes = new Map()
|
|
102
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
|
+
}, []);
|
|
103
124
|
if (!selectedNode || !selectedNode.data) {
|
|
104
125
|
return /*#__PURE__*/React.createElement(Box, {
|
|
105
126
|
flexDirection: "column"
|
|
@@ -109,16 +130,20 @@ export const TreeMapView = ({
|
|
|
109
130
|
}, "Error: Invalid node selected"), /*#__PURE__*/React.createElement(Text, {
|
|
110
131
|
dimColor: true,
|
|
111
132
|
color: oneHunter.textAlt
|
|
112
|
-
}, "Press
|
|
133
|
+
}, "Press any key to go back to tree view"));
|
|
113
134
|
}
|
|
114
|
-
const width = Math.min(stdout
|
|
115
|
-
const height = Math.min(stdout
|
|
135
|
+
const width = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.columns) || 80, 120) - 4;
|
|
136
|
+
const height = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.rows) || 24, 40) - 8;
|
|
137
|
+
|
|
138
|
+
// Choose formatter based on mode
|
|
139
|
+
const formatValue = mode === 'count' ? formatCount : formatBytes;
|
|
140
|
+
const modeLabel = mode === 'count' ? 'Count' : 'Size';
|
|
116
141
|
|
|
117
142
|
// If it's a file, show just that file as a big rectangle
|
|
118
143
|
if (selectedNode.data.isFile) {
|
|
119
144
|
const color = getFileColor(selectedNode.data.name);
|
|
120
145
|
const name = selectedNode.data.name;
|
|
121
|
-
const
|
|
146
|
+
const valueStr = mode === 'count' ? '1 file' : selectedNode.data.size ? formatBytes(selectedNode.data.size) : '';
|
|
122
147
|
const grid = Array(height).fill(null).map(() => Array(width).fill(null).map(() => ({
|
|
123
148
|
char: ' ',
|
|
124
149
|
color
|
|
@@ -148,12 +173,12 @@ export const TreeMapView = ({
|
|
|
148
173
|
};
|
|
149
174
|
}
|
|
150
175
|
}
|
|
151
|
-
if (
|
|
152
|
-
const sizeX = Math.floor((width -
|
|
153
|
-
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++) {
|
|
154
179
|
if (sizeX + i > 0) {
|
|
155
180
|
grid[midY + 2][sizeX + i] = {
|
|
156
|
-
char:
|
|
181
|
+
char: valueStr[i],
|
|
157
182
|
color
|
|
158
183
|
};
|
|
159
184
|
}
|
|
@@ -193,10 +218,10 @@ export const TreeMapView = ({
|
|
|
193
218
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
194
219
|
bold: true,
|
|
195
220
|
color: oneHunter.cyan
|
|
196
|
-
}, "TreeMap View: ", name, " (File)"), /*#__PURE__*/React.createElement(Text, {
|
|
221
|
+
}, "TreeMap View (", modeLabel, "): ", name, " (File)"), /*#__PURE__*/React.createElement(Text, {
|
|
197
222
|
dimColor: true,
|
|
198
223
|
color: oneHunter.textAlt
|
|
199
|
-
}, "Press
|
|
224
|
+
}, "Press any key to go back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
|
|
200
225
|
borderStyle: "single",
|
|
201
226
|
borderColor: oneHunter.textAlt,
|
|
202
227
|
flexDirection: "column",
|
|
@@ -205,7 +230,7 @@ export const TreeMapView = ({
|
|
|
205
230
|
dimColor: true,
|
|
206
231
|
color: oneHunter.textAlt,
|
|
207
232
|
marginTop: 1
|
|
208
|
-
}, "File: ", name, " |
|
|
233
|
+
}, "File: ", name, " | ", modeLabel, ": ", valueStr || 'Unknown'));
|
|
209
234
|
}
|
|
210
235
|
|
|
211
236
|
// It's a directory - show treemap of its children
|
|
@@ -215,10 +240,10 @@ export const TreeMapView = ({
|
|
|
215
240
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
216
241
|
bold: true,
|
|
217
242
|
color: oneHunter.cyan
|
|
218
|
-
}, "TreeMap View: ", selectedNode.data.name, " (Empty Directory)"), /*#__PURE__*/React.createElement(Text, {
|
|
243
|
+
}, "TreeMap View (", modeLabel, "): ", selectedNode.data.name, " (Empty Directory)"), /*#__PURE__*/React.createElement(Text, {
|
|
219
244
|
dimColor: true,
|
|
220
245
|
color: oneHunter.textAlt
|
|
221
|
-
}, "Press
|
|
246
|
+
}, "Press any key to go back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
|
|
222
247
|
borderStyle: "single",
|
|
223
248
|
borderColor: oneHunter.textAlt,
|
|
224
249
|
marginTop: 1,
|
|
@@ -247,12 +272,18 @@ export const TreeMapView = ({
|
|
|
247
272
|
const x1 = Math.ceil(node.x1);
|
|
248
273
|
const y1 = Math.ceil(node.y1);
|
|
249
274
|
const isFile = node.data.isFile;
|
|
275
|
+
const isNodeLoading = loadingPaths.has(node.data.path);
|
|
250
276
|
const boxWidth = x1 - x0;
|
|
251
277
|
const boxHeight = y1 - y0;
|
|
252
278
|
if (boxWidth < 1 || boxHeight < 1) return;
|
|
253
279
|
|
|
254
|
-
// Color based on type
|
|
255
|
-
|
|
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
|
+
}
|
|
256
287
|
|
|
257
288
|
// Draw border
|
|
258
289
|
for (let y = y0; y < y1 && y < height; y++) {
|
|
@@ -276,14 +307,14 @@ export const TreeMapView = ({
|
|
|
276
307
|
|
|
277
308
|
// Draw label if there's room
|
|
278
309
|
const name = node.data.name;
|
|
279
|
-
const
|
|
310
|
+
const valueStr = isNodeLoading ? ` ${SPINNER_FRAMES[spinnerFrame]}` : node.value ? ` ${formatValue(node.value)}${mode === 'count' ? ' files' : ''}` : '';
|
|
280
311
|
if (boxWidth >= 8 && boxHeight >= 2) {
|
|
281
312
|
const labelY = y0 + 1;
|
|
282
313
|
const labelX = x0 + 1;
|
|
283
314
|
const maxLength = boxWidth - 2;
|
|
284
315
|
|
|
285
|
-
// Combine name and
|
|
286
|
-
let label = name +
|
|
316
|
+
// Combine name and value
|
|
317
|
+
let label = name + valueStr;
|
|
287
318
|
if (label.length > maxLength) {
|
|
288
319
|
// Try just name
|
|
289
320
|
if (name.length <= maxLength) {
|
|
@@ -307,8 +338,9 @@ export const TreeMapView = ({
|
|
|
307
338
|
const maxLength = boxWidth - 2;
|
|
308
339
|
let label;
|
|
309
340
|
if (isFile) {
|
|
341
|
+
var _name$match;
|
|
310
342
|
// Show extension for files
|
|
311
|
-
const ext = name.match(/\.([^.]{1,3})$/)
|
|
343
|
+
const ext = (_name$match = name.match(/\.([^.]{1,3})$/)) === null || _name$match === void 0 ? void 0 : _name$match[1];
|
|
312
344
|
label = ext || name.slice(0, maxLength);
|
|
313
345
|
} else {
|
|
314
346
|
// Show first few chars for directories
|
|
@@ -372,15 +404,21 @@ export const TreeMapView = ({
|
|
|
372
404
|
key: y
|
|
373
405
|
}, segments);
|
|
374
406
|
});
|
|
407
|
+
const totalLabel = mode === 'count' ? `${formatCount(root.value || 0)} files` : formatBytes(root.value || 0);
|
|
408
|
+
const isLoading = loadingPaths.size > 0;
|
|
375
409
|
return /*#__PURE__*/React.createElement(Box, {
|
|
376
410
|
flexDirection: "column"
|
|
377
411
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
378
412
|
bold: true,
|
|
379
413
|
color: oneHunter.cyan
|
|
380
|
-
}, "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, {
|
|
381
417
|
dimColor: true,
|
|
382
418
|
color: oneHunter.textAlt
|
|
383
|
-
}, "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, {
|
|
384
422
|
borderStyle: "single",
|
|
385
423
|
borderColor: oneHunter.textAlt,
|
|
386
424
|
flexDirection: "column",
|
|
@@ -389,5 +427,5 @@ export const TreeMapView = ({
|
|
|
389
427
|
dimColor: true,
|
|
390
428
|
color: oneHunter.textAlt,
|
|
391
429
|
marginTop: 1
|
|
392
|
-
}, "Showing ", firstLevelNodes.length, " items | Total:",
|
|
430
|
+
}, "Showing ", firstLevelNodes.length, " items | Total: ", totalLabel));
|
|
393
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)));
|
|
@@ -262,7 +308,7 @@ const TreeVisualization = ({
|
|
|
262
308
|
const {
|
|
263
309
|
stdout
|
|
264
310
|
} = useStdout();
|
|
265
|
-
const terminalHeight = stdout
|
|
311
|
+
const terminalHeight = (stdout === null || stdout === void 0 ? void 0 : stdout.rows) || 24;
|
|
266
312
|
const viewportHeight = Math.max(5, terminalHeight - 7);
|
|
267
313
|
|
|
268
314
|
// Spinner animation loop
|
|
@@ -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);
|
|
@@ -559,8 +724,9 @@ const TreeVisualization = ({
|
|
|
559
724
|
// If it's a directory with no children loaded yet, traverse into it
|
|
560
725
|
if (node.data.children === undefined) {
|
|
561
726
|
await traverseDir(node);
|
|
562
|
-
} else
|
|
563
|
-
//
|
|
727
|
+
} else {
|
|
728
|
+
// ← Changed from `else if (node.children)`
|
|
729
|
+
// Children already loaded, toggle collapse (even if empty)
|
|
564
730
|
setCollapsed(prev => {
|
|
565
731
|
const next = new Set(prev);
|
|
566
732
|
if (next.has(node.data.path)) next.delete(node.data.path);else next.add(node.data.path);
|
|
@@ -596,31 +762,52 @@ const TreeVisualization = ({
|
|
|
596
762
|
|
|
597
763
|
// Set value on the DATA object
|
|
598
764
|
const data = hierarchyNode.data;
|
|
599
|
-
if (
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
data.value =
|
|
604
|
-
} else {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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 {
|
|
608
777
|
data.value = 0;
|
|
609
778
|
}
|
|
610
|
-
}
|
|
611
|
-
} else if (data.children === undefined || !hierarchyNode.children || hierarchyNode.children.length === 0) {
|
|
612
|
-
// Directory with no children loaded OR empty directory
|
|
613
|
-
// Use calculatedSize if available
|
|
614
|
-
const calcSize = calculatedSizes.get(data.path);
|
|
615
|
-
if (calcSize !== undefined) {
|
|
616
|
-
data.value = calcSize;
|
|
617
779
|
} else {
|
|
618
|
-
//
|
|
619
|
-
data.value =
|
|
780
|
+
// Directory with loaded children - let d3's .sum() aggregate from children
|
|
781
|
+
data.value = undefined;
|
|
620
782
|
}
|
|
621
783
|
} else {
|
|
622
|
-
//
|
|
623
|
-
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
|
+
}
|
|
624
811
|
}
|
|
625
812
|
};
|
|
626
813
|
populateValues(node);
|
|
@@ -628,7 +815,9 @@ const TreeVisualization = ({
|
|
|
628
815
|
selectedNode: node,
|
|
629
816
|
stdout: stdout,
|
|
630
817
|
calculatedSizes: calculatedSizes,
|
|
631
|
-
|
|
818
|
+
calculatedCounts: calculatedCounts,
|
|
819
|
+
loadingPaths: loadingPaths,
|
|
820
|
+
mode: treeMapMode
|
|
632
821
|
});
|
|
633
822
|
}
|
|
634
823
|
const start = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
|
|
@@ -642,7 +831,7 @@ const TreeVisualization = ({
|
|
|
642
831
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
643
832
|
color: oneHunter.textAlt,
|
|
644
833
|
dimColor: true
|
|
645
|
-
}, "\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, {
|
|
646
835
|
color: oneHunter.yellow
|
|
647
836
|
}, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
|
|
648
837
|
key: start + i,
|
|
@@ -652,6 +841,7 @@ const TreeVisualization = ({
|
|
|
652
841
|
shortcutInput: shortcutInput,
|
|
653
842
|
noSize: noSize,
|
|
654
843
|
calculatedSizes: calculatedSizes,
|
|
844
|
+
calculatedCounts: calculatedCounts,
|
|
655
845
|
loadingPaths: loadingPaths,
|
|
656
846
|
spinnerFrame: spinnerFrame,
|
|
657
847
|
lastModifiedDates: lastModifiedDates
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tri-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"description": "Interactive CLI directory tree visualizer with treemap view",
|
|
5
5
|
"author": "Jared Wilber",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"keywords": [
|
|
8
9
|
"cli",
|
|
9
10
|
"tree",
|
|
@@ -19,7 +20,6 @@
|
|
|
19
20
|
"bin": {
|
|
20
21
|
"tri": "dist/cli.js"
|
|
21
22
|
},
|
|
22
|
-
"type": "module",
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=16"
|
|
25
25
|
},
|
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"d3-hierarchy": "^3.1.2",
|
|
38
|
-
"ink": "^
|
|
39
|
-
"meow": "^
|
|
38
|
+
"ink": "^3.2.0",
|
|
39
|
+
"meow": "^9.0.0",
|
|
40
40
|
"react": "^18.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@babel/cli": "^7.21.0",
|
|
44
|
+
"@babel/preset-env": "^7.22.0",
|
|
44
45
|
"@babel/preset-react": "^7.28.5",
|
|
45
46
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
46
47
|
"ava": "^5.2.0",
|
|
@@ -72,7 +73,16 @@
|
|
|
72
73
|
"prettier": "@vdemedes/prettier-config",
|
|
73
74
|
"babel": {
|
|
74
75
|
"presets": [
|
|
76
|
+
[
|
|
77
|
+
"@babel/preset-env",
|
|
78
|
+
{
|
|
79
|
+
"targets": {
|
|
80
|
+
"node": "16"
|
|
81
|
+
},
|
|
82
|
+
"modules": false
|
|
83
|
+
}
|
|
84
|
+
],
|
|
75
85
|
"@babel/preset-react"
|
|
76
86
|
]
|
|
77
87
|
}
|
|
78
|
-
}
|
|
88
|
+
}
|
package/readme.tape
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Output
|
|
1
|
+
Output readme2.gif
|
|
2
2
|
Set FontSize 18
|
|
3
3
|
Set Width 1000
|
|
4
4
|
Set Height 600
|
|
@@ -10,40 +10,36 @@ Sleep 500ms
|
|
|
10
10
|
|
|
11
11
|
Type "tri ."
|
|
12
12
|
Enter
|
|
13
|
-
Sleep
|
|
13
|
+
Sleep 1s
|
|
14
14
|
|
|
15
|
-
Down
|
|
16
|
-
Sleep 250ms
|
|
17
|
-
Down
|
|
18
|
-
Sleep 250ms
|
|
19
|
-
Enter
|
|
20
|
-
Sleep 500ms
|
|
21
|
-
Type "d"
|
|
22
|
-
Enter
|
|
23
|
-
Sleep 500ms
|
|
24
15
|
Down
|
|
25
16
|
Sleep 250ms
|
|
26
17
|
Down
|
|
27
18
|
Sleep 250ms
|
|
28
19
|
Down
|
|
29
20
|
Sleep 250ms
|
|
30
|
-
Type "l"
|
|
31
|
-
Sleep 1.5s
|
|
32
|
-
|
|
33
21
|
Enter
|
|
22
|
+
Sleep 1s
|
|
23
|
+
Type "d"
|
|
34
24
|
Sleep 1.5s
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Type "m"
|
|
38
|
-
Sleep 500ms
|
|
39
|
-
Down
|
|
40
|
-
Sleep 250ms
|
|
41
|
-
Down
|
|
25
|
+
|
|
26
|
+
Type "a"
|
|
42
27
|
Sleep 250ms
|
|
28
|
+
Type "e"
|
|
29
|
+
Sleep 500ms
|
|
43
30
|
Enter
|
|
44
|
-
Sleep
|
|
31
|
+
Sleep 1s
|
|
45
32
|
|
|
46
33
|
|
|
34
|
+
Type "a"
|
|
35
|
+
Sleep 250ms
|
|
36
|
+
Type "e"
|
|
37
|
+
Sleep 500ms
|
|
38
|
+
Type "b"
|
|
39
|
+
Sleep 500ms
|
|
40
|
+
Enter
|
|
41
|
+
Sleep 1s
|
|
42
|
+
|
|
47
43
|
# Toggle treemap view
|
|
48
44
|
Type "m"
|
|
49
45
|
Sleep 2s
|
|
@@ -54,20 +50,11 @@ Sleep 2s
|
|
|
54
50
|
|
|
55
51
|
Type "a"
|
|
56
52
|
Sleep 250ms
|
|
57
|
-
Type "
|
|
58
|
-
Sleep 500ms
|
|
59
|
-
Enter
|
|
60
|
-
Sleep 1s
|
|
61
|
-
|
|
62
|
-
Type "a"
|
|
63
|
-
Sleep 250ms
|
|
64
|
-
Type "h"
|
|
53
|
+
Type "e"
|
|
65
54
|
Sleep 500ms
|
|
66
55
|
Enter
|
|
67
56
|
Sleep 1s
|
|
68
57
|
|
|
69
|
-
Enter
|
|
70
|
-
Sleep 1s
|
|
71
58
|
|
|
72
59
|
# Quit
|
|
73
60
|
Type "q"
|