tri-cli 0.0.4 → 0.0.52
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/README.md +91 -28
- package/dist/Treemap.js +6 -4
- package/dist/cli.js +168 -40
- package/package.json +16 -7
- package/readme.tape +65 -0
package/README.md
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
# tri-cli
|
|
2
2
|
|
|
3
|
-
A terminal-based interactive directory visualizer, like `tree` but with keyboard
|
|
3
|
+
A terminal-based interactive directory visualizer, like `tree` but with keyboard navigation, shortcuts, colors, and [treemap](https://en.wikipedia.org/wiki/Treemapping) visuals:
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
10
|
+
### Global Installation (Recommended)
|
|
11
|
+
|
|
12
|
+
It's recommended to install globally:
|
|
13
|
+
|
|
9
14
|
```bash
|
|
10
15
|
npm install -g tri-cli
|
|
11
16
|
```
|
|
12
17
|
|
|
18
|
+
After global installation, you can run `tri` directly from anywhere:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
tri .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Local Installation
|
|
25
|
+
|
|
26
|
+
If you don't want to install globally, you can install locally and then run anywhere with `npx`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install tri-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For local installations, use `npx` to run the command:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx tri .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or add it to your `package.json` scripts:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"scripts": {
|
|
43
|
+
"tree": "tri"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then run with `npm run tree`.
|
|
49
|
+
|
|
13
50
|
## Usage
|
|
14
51
|
|
|
15
52
|
```bash
|
|
@@ -22,46 +59,72 @@ tri [directory] [options]
|
|
|
22
59
|
tri . # visualize current directory
|
|
23
60
|
tri ../bionemo -L 2 # show 2 levels deep
|
|
24
61
|
tri --dir src # specify directory with flag
|
|
25
|
-
tri
|
|
62
|
+
tri src -s # preload all sizes (enables immediate treemap)
|
|
63
|
+
tri --dir src -c # start with all directories collapsed
|
|
26
64
|
```
|
|
27
65
|
|
|
28
66
|
### Interactive Controls
|
|
29
67
|
|
|
30
|
-
| Key
|
|
31
|
-
|
|
|
32
|
-
| ↑ / ↓
|
|
33
|
-
| Enter
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
| Key | Action |
|
|
69
|
+
| ------------- | ----------------------------------------- |
|
|
70
|
+
| ↑ / ↓ | Navigate files and folders |
|
|
71
|
+
| Enter | Expand or collapse a directory |
|
|
72
|
+
| [a-z]/[0-9] | Type shortcut to jump to item |
|
|
73
|
+
| d | Calculate and display sizes recursively |
|
|
74
|
+
| l | Show last modified dates (ls -l style) |
|
|
75
|
+
| m | Toggle treemap view |
|
|
76
|
+
| Esc | Clear typed shortcut |
|
|
77
|
+
| q | Quit |
|
|
78
|
+
|
|
79
|
+
### Command Line Options
|
|
80
|
+
|
|
81
|
+
| Option | Description |
|
|
82
|
+
| ---------------- | ------------------------------------------------ |
|
|
83
|
+
| `[directory]` | Directory to visualize (default: current dir) |
|
|
84
|
+
| `-L <level>` | Maximum depth to load initially (default: 1) |
|
|
85
|
+
| `-s, --size` | Preload all file/directory sizes |
|
|
86
|
+
| `-c, --collapse` | Start with all directories collapsed |
|
|
87
|
+
| `-h, --help` | Show help message |
|
|
88
|
+
|
|
89
|
+
## Features
|
|
90
|
+
|
|
91
|
+
### Tree View
|
|
92
|
+
- **Keyboard shortcuts**: Each item gets a shortcut label - just type it to jump there instantly
|
|
93
|
+
- **Tree-style lines**: Visual hierarchy with Unicode box-drawing characters (├─, ╰─, │)
|
|
94
|
+
- **Color coding**: Different colors for different file types
|
|
95
|
+
- **Lazy loading**: Only loads directory contents when you expand them (fast for large directories)
|
|
96
|
+
- **Size calculation**: Press `d` on any item to calculate disk usage recursively
|
|
97
|
+
- **Date display**: Press `l` to show last modified dates in `ls -l` format
|
|
98
|
+
|
|
99
|
+
### Treemap View
|
|
100
|
+
- Press `m` to visualize the selected directory as a treemap
|
|
101
|
+
- Visual representation of file/directory sizes
|
|
102
|
+
- Hierarchical rectangles sized proportionally to disk usage
|
|
103
|
+
- Color-coded by file type or directory group
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
Clone the repository and install dependencies:
|
|
48
108
|
|
|
49
109
|
```bash
|
|
50
|
-
|
|
110
|
+
git clone https://github.com/jwilber/tri-cli.git
|
|
111
|
+
cd tri-cli
|
|
112
|
+
npm install
|
|
51
113
|
```
|
|
52
114
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
**Example:**
|
|
56
|
-
|
|
57
|
-
It's probably easiest to just uninstall and link:
|
|
115
|
+
Build and link for local development:
|
|
58
116
|
|
|
59
117
|
```bash
|
|
60
118
|
npm run build
|
|
61
|
-
|
|
62
119
|
npm link
|
|
63
120
|
```
|
|
64
121
|
|
|
65
|
-
|
|
122
|
+
Now you can run `tri` from anywhere and it will use your local development version.
|
|
123
|
+
|
|
124
|
+
## License
|
|
66
125
|
|
|
67
126
|
MIT
|
|
127
|
+
|
|
128
|
+
## Author
|
|
129
|
+
|
|
130
|
+
Jared Wilber
|
package/dist/Treemap.js
CHANGED
|
@@ -32,7 +32,8 @@ const formatBytes = bytes => {
|
|
|
32
32
|
|
|
33
33
|
// Get color based on file extension
|
|
34
34
|
const getFileColor = filename => {
|
|
35
|
-
|
|
35
|
+
var _filename$match;
|
|
36
|
+
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
37
|
const colorMap = {
|
|
37
38
|
'.js': oneHunter.yellow,
|
|
38
39
|
'.jsx': oneHunter.yellow,
|
|
@@ -111,8 +112,8 @@ export const TreeMapView = ({
|
|
|
111
112
|
color: oneHunter.textAlt
|
|
112
113
|
}, "Press 't' to go back to tree view"));
|
|
113
114
|
}
|
|
114
|
-
const width = Math.min(stdout
|
|
115
|
-
const height = Math.min(stdout
|
|
115
|
+
const width = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.columns) || 80, 120) - 4;
|
|
116
|
+
const height = Math.min((stdout === null || stdout === void 0 ? void 0 : stdout.rows) || 24, 40) - 8;
|
|
116
117
|
|
|
117
118
|
// If it's a file, show just that file as a big rectangle
|
|
118
119
|
if (selectedNode.data.isFile) {
|
|
@@ -307,8 +308,9 @@ export const TreeMapView = ({
|
|
|
307
308
|
const maxLength = boxWidth - 2;
|
|
308
309
|
let label;
|
|
309
310
|
if (isFile) {
|
|
311
|
+
var _name$match;
|
|
310
312
|
// Show extension for files
|
|
311
|
-
const ext = name.match(/\.([^.]{1,3})$/)
|
|
313
|
+
const ext = (_name$match = name.match(/\.([^.]{1,3})$/)) === null || _name$match === void 0 ? void 0 : _name$match[1];
|
|
312
314
|
label = ext || name.slice(0, maxLength);
|
|
313
315
|
} else {
|
|
314
316
|
// Show first few chars for directories
|
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,24 @@ 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 formatDate = timestamp => {
|
|
34
|
+
const date = new Date(timestamp);
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
|
37
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
38
|
+
const month = months[date.getMonth()];
|
|
39
|
+
const day = date.getDate().toString().padStart(2, ' ');
|
|
40
|
+
|
|
41
|
+
// If file is from this year, show time; otherwise show year
|
|
42
|
+
if (diffDays < 180) {
|
|
43
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
|
44
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
45
|
+
return `${month} ${day} ${hours}:${minutes}`;
|
|
46
|
+
} else {
|
|
47
|
+
const year = date.getFullYear();
|
|
48
|
+
return `${month} ${day} ${year}`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
33
51
|
|
|
34
52
|
// Async non-blocking directory size
|
|
35
53
|
const getDirSizeAsync = async dirPath => {
|
|
@@ -117,7 +135,7 @@ const getFileColor = filename => {
|
|
|
117
135
|
|
|
118
136
|
// Exclude m, t, d
|
|
119
137
|
const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
120
|
-
const letters = '
|
|
138
|
+
const letters = 'abcefgijknopqrsuvwxyz';
|
|
121
139
|
const base = letters.length;
|
|
122
140
|
if (isFile) return `${parentShortcut}${index + 1}`;
|
|
123
141
|
let label = '';
|
|
@@ -128,6 +146,34 @@ const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
|
128
146
|
}
|
|
129
147
|
return parentShortcut + label;
|
|
130
148
|
};
|
|
149
|
+
|
|
150
|
+
// Generate tree lines (like the `tree` command)
|
|
151
|
+
const getTreeLines = node => {
|
|
152
|
+
if (node.depth === 0) return ''; // Root has no lines
|
|
153
|
+
|
|
154
|
+
const lines = [];
|
|
155
|
+
let current = node;
|
|
156
|
+
|
|
157
|
+
// Build the lines from current node up to (but not including) root
|
|
158
|
+
while (current.parent && current.parent.depth > 0) {
|
|
159
|
+
const parent = current.parent;
|
|
160
|
+
const isLastChild = parent.children && current === parent.children[parent.children.length - 1];
|
|
161
|
+
lines.unshift(isLastChild ? ' ' : '│ ');
|
|
162
|
+
current = parent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add the final connector for this node
|
|
166
|
+
const parent = node.parent;
|
|
167
|
+
if (parent && parent.depth === 0) {
|
|
168
|
+
// Direct child of root
|
|
169
|
+
const isLastChild = parent.children && node === parent.children[parent.children.length - 1];
|
|
170
|
+
return isLastChild ? '╰─' : '├─';
|
|
171
|
+
} else if (parent) {
|
|
172
|
+
const isLastChild = parent.children && node === parent.children[parent.children.length - 1];
|
|
173
|
+
return lines.join('') + (isLastChild ? '╰─' : '├─');
|
|
174
|
+
}
|
|
175
|
+
return '';
|
|
176
|
+
};
|
|
131
177
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
132
178
|
const TreeRow = /*#__PURE__*/React.memo(({
|
|
133
179
|
node,
|
|
@@ -137,12 +183,15 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
137
183
|
noSize,
|
|
138
184
|
calculatedSizes,
|
|
139
185
|
loadingPaths,
|
|
140
|
-
spinnerFrame
|
|
186
|
+
spinnerFrame,
|
|
187
|
+
lastModifiedDates
|
|
141
188
|
}) => {
|
|
142
189
|
const isCollapsed = collapsed.has(node.data.path);
|
|
143
190
|
const hasChildren = node.children && node.children.length > 0;
|
|
144
191
|
const isLoading = loadingPaths.has(node.data.path);
|
|
145
192
|
const fileColor = node.data.isFile ? getFileColor(node.data.name) : oneHunter.magenta;
|
|
193
|
+
const isRoot = node.depth === 0;
|
|
194
|
+
const isDirectory = !node.data.isFile;
|
|
146
195
|
let displaySize = 0;
|
|
147
196
|
// Use calculated size if available, otherwise fall back to node.data.size
|
|
148
197
|
if (calculatedSizes && calculatedSizes.has(node.data.path)) {
|
|
@@ -150,6 +199,8 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
150
199
|
} else {
|
|
151
200
|
displaySize = node.data.size;
|
|
152
201
|
}
|
|
202
|
+
const lastModified = lastModifiedDates.get(node.data.path);
|
|
203
|
+
const treeLines = getTreeLines(node);
|
|
153
204
|
|
|
154
205
|
// ✅ Restore highlight logic
|
|
155
206
|
const renderShortcut = () => {
|
|
@@ -172,21 +223,25 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
172
223
|
dimColor: true
|
|
173
224
|
}, ' ', "[", node.shortcut, "]");
|
|
174
225
|
};
|
|
175
|
-
return /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
|
|
226
|
+
return /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, !isRoot && /*#__PURE__*/React.createElement(Text, {
|
|
227
|
+
color: oneHunter.textAlt
|
|
228
|
+
}, treeLines), !isRoot && isDirectory && /*#__PURE__*/React.createElement(Text, {
|
|
176
229
|
color: oneHunter.textAlt
|
|
177
|
-
},
|
|
178
|
-
color:
|
|
179
|
-
},
|
|
230
|
+
}, hasChildren && !isCollapsed ? '▼ ' : '▶ '), !isRoot && !isDirectory && /*#__PURE__*/React.createElement(Text, {
|
|
231
|
+
color: oneHunter.textAlt
|
|
232
|
+
}, "\u2500 "), /*#__PURE__*/React.createElement(Text, {
|
|
180
233
|
bold: isSelected,
|
|
181
234
|
color: isSelected ? oneHunter.bg : fileColor,
|
|
182
235
|
backgroundColor: isSelected ? oneHunter.green : undefined
|
|
183
|
-
}, node.data.name), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
236
|
+
}, node.data.name, !node.data.isFile && !isRoot ? '/' : ''), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
184
237
|
color: oneHunter.textAlt
|
|
185
238
|
}, " (", node.children.length, ")"), isLoading ? /*#__PURE__*/React.createElement(Text, {
|
|
186
239
|
color: oneHunter.yellow
|
|
187
240
|
}, " ", SPINNER_FRAMES[spinnerFrame]) : displaySize > 0 ? /*#__PURE__*/React.createElement(Text, {
|
|
188
241
|
color: oneHunter.cyan
|
|
189
|
-
}, " ", formatBytes(displaySize)) : /*#__PURE__*/React.createElement(Text, null, " "), renderShortcut()
|
|
242
|
+
}, " ", formatBytes(displaySize)) : /*#__PURE__*/React.createElement(Text, null, " "), renderShortcut(), lastModified && /*#__PURE__*/React.createElement(Text, {
|
|
243
|
+
color: oneHunter.text2
|
|
244
|
+
}, " ", formatDate(lastModified))));
|
|
190
245
|
});
|
|
191
246
|
const TreeVisualization = ({
|
|
192
247
|
dirPath = '.',
|
|
@@ -203,10 +258,11 @@ const TreeVisualization = ({
|
|
|
203
258
|
const [loadingPaths, setLoadingPaths] = useState(new Set());
|
|
204
259
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
205
260
|
const [rootTree, setRootTree] = useState(() => hierarchy(readDirTree(dirPath, noSize, 0, maxLevel)));
|
|
261
|
+
const [lastModifiedDates, setLastModifiedDates] = useState(new Map());
|
|
206
262
|
const {
|
|
207
263
|
stdout
|
|
208
264
|
} = useStdout();
|
|
209
|
-
const terminalHeight = stdout
|
|
265
|
+
const terminalHeight = (stdout === null || stdout === void 0 ? void 0 : stdout.rows) || 24;
|
|
210
266
|
const viewportHeight = Math.max(5, terminalHeight - 7);
|
|
211
267
|
|
|
212
268
|
// Spinner animation loop
|
|
@@ -329,6 +385,44 @@ const TreeVisualization = ({
|
|
|
329
385
|
await new Promise(r => setTimeout(r, 0));
|
|
330
386
|
}
|
|
331
387
|
};
|
|
388
|
+
const getLastModifiedAsync = async node => {
|
|
389
|
+
// Collect node and all its descendants
|
|
390
|
+
const nodesToProcess = [];
|
|
391
|
+
const collectNodes = n => {
|
|
392
|
+
// Skip if already have date
|
|
393
|
+
if (lastModifiedDates.has(n.data.path)) {
|
|
394
|
+
// Still recurse to children in case they need dates
|
|
395
|
+
if (n.children) {
|
|
396
|
+
n.children.forEach(collectNodes);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
nodesToProcess.push(n);
|
|
401
|
+
if (n.children) {
|
|
402
|
+
n.children.forEach(collectNodes);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
collectNodes(node);
|
|
406
|
+
if (nodesToProcess.length === 0) return; // Nothing to process
|
|
407
|
+
|
|
408
|
+
const newMap = new Map(lastModifiedDates);
|
|
409
|
+
|
|
410
|
+
// Get mtime for each node
|
|
411
|
+
for (const targetNode of nodesToProcess) {
|
|
412
|
+
try {
|
|
413
|
+
const stats = fs.statSync(targetNode.data.path);
|
|
414
|
+
newMap.set(targetNode.data.path, stats.mtimeMs);
|
|
415
|
+
} catch {
|
|
416
|
+
// If we can't stat the file, skip it
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Stream the update immediately
|
|
420
|
+
React.startTransition(() => {
|
|
421
|
+
setLastModifiedDates(new Map(newMap));
|
|
422
|
+
});
|
|
423
|
+
await new Promise(r => setTimeout(r, 0));
|
|
424
|
+
}
|
|
425
|
+
};
|
|
332
426
|
const traverseDir = async node => {
|
|
333
427
|
if (node.data.isFile) return;
|
|
334
428
|
|
|
@@ -425,18 +519,11 @@ const TreeVisualization = ({
|
|
|
425
519
|
});
|
|
426
520
|
return;
|
|
427
521
|
}
|
|
428
|
-
if (input === 't') {
|
|
429
|
-
const node = visibleNodes[selectedIndex];
|
|
430
|
-
if (node) await traverseDir(node);
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
522
|
if (input === 'm') {
|
|
434
523
|
const node = visibleNodes[selectedIndex];
|
|
435
524
|
if (!node) return;
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
calculateSizeAsync(node);
|
|
439
|
-
}
|
|
525
|
+
// Always start calculating sizes in the background (calculateSizeAsync skips already-calculated nodes)
|
|
526
|
+
calculateSizeAsync(node);
|
|
440
527
|
// Show treemap immediately
|
|
441
528
|
setShowTreeMap(true);
|
|
442
529
|
return;
|
|
@@ -446,6 +533,11 @@ const TreeVisualization = ({
|
|
|
446
533
|
if (node) await calculateSizeAsync(node);
|
|
447
534
|
return;
|
|
448
535
|
}
|
|
536
|
+
if (input === 'l') {
|
|
537
|
+
const node = visibleNodes[selectedIndex];
|
|
538
|
+
if (node) await getLastModifiedAsync(node);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
449
541
|
if (key.upArrow) {
|
|
450
542
|
setSelectedIndex(p => Math.max(0, p - 1));
|
|
451
543
|
setShortcutInput('');
|
|
@@ -458,11 +550,24 @@ const TreeVisualization = ({
|
|
|
458
550
|
}
|
|
459
551
|
if (key.return) {
|
|
460
552
|
const node = visibleNodes[selectedIndex];
|
|
461
|
-
if (node
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
553
|
+
if (!node) return;
|
|
554
|
+
|
|
555
|
+
// Don't allow collapsing/expanding the root
|
|
556
|
+
if (node.depth === 0) return;
|
|
557
|
+
if (node.data.isFile) return; // Do nothing for files
|
|
558
|
+
|
|
559
|
+
// If it's a directory with no children loaded yet, traverse into it
|
|
560
|
+
if (node.data.children === undefined) {
|
|
561
|
+
await traverseDir(node);
|
|
562
|
+
} else {
|
|
563
|
+
// ← Changed from `else if (node.children)`
|
|
564
|
+
// Children already loaded, toggle collapse (even if empty)
|
|
565
|
+
setCollapsed(prev => {
|
|
566
|
+
const next = new Set(prev);
|
|
567
|
+
if (next.has(node.data.path)) next.delete(node.data.path);else next.add(node.data.path);
|
|
568
|
+
return next;
|
|
569
|
+
});
|
|
570
|
+
}
|
|
466
571
|
setShortcutInput('');
|
|
467
572
|
return;
|
|
468
573
|
}
|
|
@@ -479,22 +584,44 @@ const TreeVisualization = ({
|
|
|
479
584
|
if (showTreeMap) {
|
|
480
585
|
const node = visibleNodes[selectedIndex];
|
|
481
586
|
|
|
482
|
-
// Populate value property for treemap visualization
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
} else {
|
|
492
|
-
// For directories without calculated size, use 0 (will be updated as data streams in)
|
|
493
|
-
n.data.value = 0;
|
|
587
|
+
// Populate value property on DATA objects for treemap visualization
|
|
588
|
+
// This is important because TreeMapView creates a new hierarchy from node.data,
|
|
589
|
+
// so we need to set values on the data objects, not the hierarchy nodes
|
|
590
|
+
const populateValues = hierarchyNode => {
|
|
591
|
+
if (!hierarchyNode) return;
|
|
592
|
+
|
|
593
|
+
// Recursively populate children first (depth-first, bottom-up)
|
|
594
|
+
if (hierarchyNode.children) {
|
|
595
|
+
hierarchyNode.children.forEach(populateValues);
|
|
494
596
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
597
|
+
|
|
598
|
+
// Set value on the DATA object
|
|
599
|
+
const data = hierarchyNode.data;
|
|
600
|
+
if (data.isFile) {
|
|
601
|
+
// Files: use calculatedSize or read from disk
|
|
602
|
+
const calcSize = calculatedSizes.get(data.path);
|
|
603
|
+
if (calcSize !== undefined) {
|
|
604
|
+
data.value = calcSize;
|
|
605
|
+
} else {
|
|
606
|
+
try {
|
|
607
|
+
data.value = fs.statSync(data.path).size;
|
|
608
|
+
} catch {
|
|
609
|
+
data.value = 0;
|
|
610
|
+
}
|
|
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
|
+
} else {
|
|
619
|
+
// For unloaded directories without calculated size, will show as 0
|
|
620
|
+
data.value = data.size || 0;
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
// Directory with loaded children - let d3's .sum() aggregate from children
|
|
624
|
+
data.value = undefined;
|
|
498
625
|
}
|
|
499
626
|
};
|
|
500
627
|
populateValues(node);
|
|
@@ -516,7 +643,7 @@ const TreeVisualization = ({
|
|
|
516
643
|
}, /*#__PURE__*/React.createElement(Text, {
|
|
517
644
|
color: oneHunter.textAlt,
|
|
518
645
|
dimColor: true
|
|
519
|
-
}, "\u2191/\u2193: Navigate | Enter:
|
|
646
|
+
}, "\u2191/\u2193: Navigate | Enter: Expand/Traverse | d: Size | l: Date | m: Map | q: Quit"), loadingPaths.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
520
647
|
color: oneHunter.yellow
|
|
521
648
|
}, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
|
|
522
649
|
key: start + i,
|
|
@@ -527,7 +654,8 @@ const TreeVisualization = ({
|
|
|
527
654
|
noSize: noSize,
|
|
528
655
|
calculatedSizes: calculatedSizes,
|
|
529
656
|
loadingPaths: loadingPaths,
|
|
530
|
-
spinnerFrame: spinnerFrame
|
|
657
|
+
spinnerFrame: spinnerFrame,
|
|
658
|
+
lastModifiedDates: lastModifiedDates
|
|
531
659
|
})));
|
|
532
660
|
};
|
|
533
661
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tri-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52",
|
|
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
|
},
|
|
@@ -28,20 +28,20 @@
|
|
|
28
28
|
"postbuild": "chmod +x dist/cli.js",
|
|
29
29
|
"dev": "babel --out-dir=dist --watch source",
|
|
30
30
|
"test": "prettier --check . && xo && ava",
|
|
31
|
-
"test:local": "npm run build &&
|
|
31
|
+
"test:local": "npm run build && node dist/cli.js --dir . -L 2"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"d3-hierarchy": "^3.1.2",
|
|
38
|
-
"ink": "^
|
|
39
|
-
"meow": "^
|
|
40
|
-
"react": "^18.2.0"
|
|
41
|
-
"tri-cli": "^0.0.3"
|
|
38
|
+
"ink": "^3.2.0",
|
|
39
|
+
"meow": "^9.0.0",
|
|
40
|
+
"react": "^18.2.0"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
43
|
"@babel/cli": "^7.21.0",
|
|
44
|
+
"@babel/preset-env": "^7.22.0",
|
|
45
45
|
"@babel/preset-react": "^7.28.5",
|
|
46
46
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
47
47
|
"ava": "^5.2.0",
|
|
@@ -73,6 +73,15 @@
|
|
|
73
73
|
"prettier": "@vdemedes/prettier-config",
|
|
74
74
|
"babel": {
|
|
75
75
|
"presets": [
|
|
76
|
+
[
|
|
77
|
+
"@babel/preset-env",
|
|
78
|
+
{
|
|
79
|
+
"targets": {
|
|
80
|
+
"node": "16"
|
|
81
|
+
},
|
|
82
|
+
"modules": false
|
|
83
|
+
}
|
|
84
|
+
],
|
|
76
85
|
"@babel/preset-react"
|
|
77
86
|
]
|
|
78
87
|
}
|
package/readme.tape
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Output readme2.gif
|
|
2
|
+
Set FontSize 18
|
|
3
|
+
Set Width 1000
|
|
4
|
+
Set Height 600
|
|
5
|
+
Set Theme "Dracula"
|
|
6
|
+
|
|
7
|
+
Type "# tri: interactive tree + treemap visual"
|
|
8
|
+
Enter
|
|
9
|
+
Sleep 500ms
|
|
10
|
+
|
|
11
|
+
Type "tri ."
|
|
12
|
+
Enter
|
|
13
|
+
Sleep 1s
|
|
14
|
+
|
|
15
|
+
Down
|
|
16
|
+
Sleep 250ms
|
|
17
|
+
Down
|
|
18
|
+
Sleep 250ms
|
|
19
|
+
Down
|
|
20
|
+
Sleep 250ms
|
|
21
|
+
Enter
|
|
22
|
+
Sleep 1s
|
|
23
|
+
Type "d"
|
|
24
|
+
Sleep 1.5s
|
|
25
|
+
|
|
26
|
+
Type "a"
|
|
27
|
+
Sleep 250ms
|
|
28
|
+
Type "e"
|
|
29
|
+
Sleep 500ms
|
|
30
|
+
Enter
|
|
31
|
+
Sleep 1s
|
|
32
|
+
|
|
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
|
+
|
|
43
|
+
# Toggle treemap view
|
|
44
|
+
Type "m"
|
|
45
|
+
Sleep 2s
|
|
46
|
+
|
|
47
|
+
# Return to tree view
|
|
48
|
+
Type "m"
|
|
49
|
+
Sleep 2s
|
|
50
|
+
|
|
51
|
+
Type "a"
|
|
52
|
+
Sleep 250ms
|
|
53
|
+
Type "e"
|
|
54
|
+
Sleep 500ms
|
|
55
|
+
Enter
|
|
56
|
+
Sleep 1s
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Quit
|
|
60
|
+
Type "q"
|
|
61
|
+
Sleep 1s
|
|
62
|
+
|
|
63
|
+
Type "# install with: npm i -g tri-cli"
|
|
64
|
+
Enter
|
|
65
|
+
Sleep 2s
|