tri-cli 0.0.3 → 0.0.51
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 +97 -18
- package/dist/Treemap.js +1 -1
- package/dist/cli.js +468 -153
- package/package.json +5 -4
- package/readme.tape +78 -0
package/README.md
CHANGED
|
@@ -1,13 +1,52 @@
|
|
|
1
1
|
# tri-cli
|
|
2
2
|
|
|
3
|
-
A terminal-based interactive directory visualizer, like `tree` but with
|
|
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
|
+
|
|
5
|
+

|
|
6
|
+
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
10
|
+
### Global Installation (Recommended)
|
|
11
|
+
|
|
12
|
+
It's recommended to install globally:
|
|
13
|
+
|
|
7
14
|
```bash
|
|
8
15
|
npm install -g tri-cli
|
|
9
16
|
```
|
|
10
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
|
+
|
|
11
50
|
## Usage
|
|
12
51
|
|
|
13
52
|
```bash
|
|
@@ -20,32 +59,72 @@ tri [directory] [options]
|
|
|
20
59
|
tri . # visualize current directory
|
|
21
60
|
tri ../bionemo -L 2 # show 2 levels deep
|
|
22
61
|
tri --dir src # specify directory with flag
|
|
62
|
+
tri src -s # preload all sizes (enables immediate treemap)
|
|
63
|
+
tri --dir src -c # start with all directories collapsed
|
|
23
64
|
```
|
|
24
65
|
|
|
25
66
|
### Interactive Controls
|
|
26
67
|
|
|
27
|
-
| Key
|
|
28
|
-
|
|
|
29
|
-
| ↑ / ↓
|
|
30
|
-
| Enter
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
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
|
|
34
90
|
|
|
35
|
-
###
|
|
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
|
|
36
98
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
104
|
|
|
43
|
-
|
|
105
|
+
## Development
|
|
44
106
|
|
|
45
|
-
|
|
107
|
+
Clone the repository and install dependencies:
|
|
46
108
|
|
|
47
109
|
```bash
|
|
48
|
-
|
|
110
|
+
git clone https://github.com/jwilber/tri-cli.git
|
|
111
|
+
cd tri-cli
|
|
112
|
+
npm install
|
|
49
113
|
```
|
|
50
114
|
|
|
51
|
-
|
|
115
|
+
Build and link for local development:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm run build
|
|
119
|
+
npm link
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Now you can run `tri` from anywhere and it will use your local development version.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
127
|
+
|
|
128
|
+
## Author
|
|
129
|
+
|
|
130
|
+
Jared Wilber
|
package/dist/Treemap.js
CHANGED
|
@@ -389,5 +389,5 @@ export const TreeMapView = ({
|
|
|
389
389
|
dimColor: true,
|
|
390
390
|
color: oneHunter.textAlt,
|
|
391
391
|
marginTop: 1
|
|
392
|
-
}, "Showing ", firstLevelNodes.length, " items | Total:
|
|
392
|
+
}, "Showing ", firstLevelNodes.length, " items | Total:", ' ', formatBytes(root.value || 0)));
|
|
393
393
|
};
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,10 @@ import { render, Text, Box, useInput, useStdout } from 'ink';
|
|
|
4
4
|
import { hierarchy } from 'd3-hierarchy';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import util from 'util';
|
|
7
9
|
import { TreeMapView } from './Treemap.js';
|
|
10
|
+
const execAsync = util.promisify(exec);
|
|
8
11
|
const oneHunter = {
|
|
9
12
|
bg: '#282c34',
|
|
10
13
|
bgAlt: '#21252b',
|
|
@@ -20,8 +23,6 @@ const oneHunter = {
|
|
|
20
23
|
purple: 'rgb(139, 126, 200)',
|
|
21
24
|
magenta: 'rgb(206, 93, 151)'
|
|
22
25
|
};
|
|
23
|
-
|
|
24
|
-
// --- Helpers ---
|
|
25
26
|
const formatBytes = bytes => {
|
|
26
27
|
if (bytes === 0) return '0 B';
|
|
27
28
|
const k = 1024;
|
|
@@ -29,55 +30,82 @@ const formatBytes = bytes => {
|
|
|
29
30
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
30
31
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
31
32
|
};
|
|
32
|
-
const
|
|
33
|
-
|
|
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
|
+
};
|
|
51
|
+
|
|
52
|
+
// Async non-blocking directory size
|
|
53
|
+
const getDirSizeAsync = async dirPath => {
|
|
34
54
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
size += getDirSize(itemPath);
|
|
44
|
-
}
|
|
45
|
-
} catch {}
|
|
55
|
+
const escapedPath = dirPath.replace(/"/g, '\\"');
|
|
56
|
+
const {
|
|
57
|
+
stdout
|
|
58
|
+
} = await execAsync(`du -sk "${escapedPath}" 2>/dev/null`);
|
|
59
|
+
const match = stdout.trim().split(/\s+/);
|
|
60
|
+
if (match.length >= 1) {
|
|
61
|
+
const sizeInKB = parseInt(match[0], 10);
|
|
62
|
+
return isNaN(sizeInKB) ? 0 : sizeInKB * 1024;
|
|
46
63
|
}
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
return 0;
|
|
65
|
+
} catch {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
49
68
|
};
|
|
50
|
-
|
|
51
|
-
// ✅ Updated: allow skipping size computation
|
|
52
|
-
const readDirTree = (dirPath, noSize = false) => {
|
|
69
|
+
const readDirTree = (dirPath, noSize = true, currentLevel = 0, maxLevel = Infinity) => {
|
|
53
70
|
try {
|
|
54
71
|
const stats = fs.statSync(dirPath);
|
|
55
|
-
|
|
72
|
+
let name = path.basename(dirPath);
|
|
73
|
+
// If basename is empty (root) or '.', use the absolute path's last component or full path
|
|
74
|
+
if (!name || name === '.') {
|
|
75
|
+
const resolved = path.resolve(dirPath);
|
|
76
|
+
name = path.basename(resolved) || resolved;
|
|
77
|
+
}
|
|
56
78
|
if (!stats.isDirectory()) {
|
|
57
79
|
const size = noSize ? 0 : stats.size;
|
|
58
80
|
return {
|
|
59
81
|
name,
|
|
60
82
|
path: dirPath,
|
|
61
83
|
isFile: true,
|
|
62
|
-
size
|
|
63
|
-
value: size > 0 ? size : 100
|
|
84
|
+
size
|
|
64
85
|
};
|
|
65
86
|
}
|
|
66
|
-
|
|
87
|
+
if (currentLevel >= maxLevel) {
|
|
88
|
+
return {
|
|
89
|
+
name,
|
|
90
|
+
path: dirPath,
|
|
91
|
+
isFile: false,
|
|
92
|
+
size: 0,
|
|
93
|
+
children: undefined
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const children = fs.readdirSync(dirPath).filter(f => !f.startsWith('.')).map(f => readDirTree(path.join(dirPath, f), noSize, currentLevel + 1, maxLevel)).filter(Boolean);
|
|
67
97
|
return {
|
|
68
98
|
name,
|
|
69
99
|
path: dirPath,
|
|
70
100
|
isFile: false,
|
|
71
|
-
size:
|
|
72
|
-
children
|
|
101
|
+
size: 0,
|
|
102
|
+
children
|
|
73
103
|
};
|
|
74
104
|
} catch {
|
|
75
105
|
return null;
|
|
76
106
|
}
|
|
77
107
|
};
|
|
78
108
|
const getFileColor = filename => {
|
|
79
|
-
if (filename === 'VERSION') return oneHunter.purple;
|
|
80
|
-
if (filename === 'Dockerfile') return oneHunter.blue;
|
|
81
109
|
const ext = path.extname(filename).toLowerCase();
|
|
82
110
|
const colorMap = {
|
|
83
111
|
'.js': oneHunter.yellow,
|
|
@@ -104,8 +132,10 @@ const getFileColor = filename => {
|
|
|
104
132
|
};
|
|
105
133
|
return colorMap[ext] || oneHunter.text;
|
|
106
134
|
};
|
|
135
|
+
|
|
136
|
+
// Exclude m, t, d
|
|
107
137
|
const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
108
|
-
const letters = '
|
|
138
|
+
const letters = 'abcefgijknopqrsuvwxyz';
|
|
109
139
|
const base = letters.length;
|
|
110
140
|
if (isFile) return `${parentShortcut}${index + 1}`;
|
|
111
141
|
let label = '';
|
|
@@ -117,160 +147,420 @@ const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
|
117
147
|
return parentShortcut + label;
|
|
118
148
|
};
|
|
119
149
|
|
|
120
|
-
//
|
|
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
|
+
};
|
|
177
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
121
178
|
const TreeRow = /*#__PURE__*/React.memo(({
|
|
122
179
|
node,
|
|
123
180
|
isSelected,
|
|
124
181
|
collapsed,
|
|
125
182
|
shortcutInput,
|
|
126
|
-
noSize
|
|
183
|
+
noSize,
|
|
184
|
+
calculatedSizes,
|
|
185
|
+
loadingPaths,
|
|
186
|
+
spinnerFrame,
|
|
187
|
+
lastModifiedDates
|
|
127
188
|
}) => {
|
|
128
189
|
const isCollapsed = collapsed.has(node.data.path);
|
|
129
190
|
const hasChildren = node.children && node.children.length > 0;
|
|
130
|
-
|
|
131
|
-
let current = node;
|
|
132
|
-
const segments = [];
|
|
133
|
-
while (current.parent) {
|
|
134
|
-
const parent = current.parent;
|
|
135
|
-
const siblings = parent.children || [];
|
|
136
|
-
const isLastChild = siblings[siblings.length - 1] === current;
|
|
137
|
-
segments.unshift(isLastChild ? ' ' : '│ ');
|
|
138
|
-
current = parent;
|
|
139
|
-
}
|
|
140
|
-
if (node.parent) {
|
|
141
|
-
const siblings = node.parent.children || [];
|
|
142
|
-
const isLastChild = siblings[siblings.length - 1] === node;
|
|
143
|
-
prefix = segments.join('').slice(0, -2) + (isLastChild ? '└─' : '├─');
|
|
144
|
-
}
|
|
145
|
-
let icon = hasChildren ? isCollapsed ? '▶ ' : '▼ ' : ' ';
|
|
191
|
+
const isLoading = loadingPaths.has(node.data.path);
|
|
146
192
|
const fileColor = node.data.isFile ? getFileColor(node.data.name) : oneHunter.magenta;
|
|
193
|
+
const isRoot = node.depth === 0;
|
|
194
|
+
const isDirectory = !node.data.isFile;
|
|
195
|
+
let displaySize = 0;
|
|
196
|
+
// Use calculated size if available, otherwise fall back to node.data.size
|
|
197
|
+
if (calculatedSizes && calculatedSizes.has(node.data.path)) {
|
|
198
|
+
displaySize = calculatedSizes.get(node.data.path);
|
|
199
|
+
} else {
|
|
200
|
+
displaySize = node.data.size;
|
|
201
|
+
}
|
|
202
|
+
const lastModified = lastModifiedDates.get(node.data.path);
|
|
203
|
+
const treeLines = getTreeLines(node);
|
|
204
|
+
|
|
205
|
+
// ✅ Restore highlight logic
|
|
147
206
|
const renderShortcut = () => {
|
|
148
207
|
if (!shortcutInput) return /*#__PURE__*/React.createElement(Text, {
|
|
149
208
|
color: oneHunter.textAlt,
|
|
150
209
|
dimColor: true
|
|
151
|
-
}, '
|
|
210
|
+
}, ' ', "[", node.shortcut, "]");
|
|
152
211
|
if (node.shortcut.startsWith(shortcutInput)) {
|
|
153
212
|
const matched = node.shortcut.slice(0, shortcutInput.length);
|
|
154
213
|
const rest = node.shortcut.slice(shortcutInput.length);
|
|
155
214
|
return /*#__PURE__*/React.createElement(Text, {
|
|
156
215
|
color: oneHunter.textAlt
|
|
157
|
-
}, ' [
|
|
216
|
+
}, ' ', "[", /*#__PURE__*/React.createElement(Text, {
|
|
158
217
|
underline: true,
|
|
159
218
|
color: oneHunter.yellow
|
|
160
|
-
}, matched), rest,
|
|
219
|
+
}, matched), rest, "]");
|
|
161
220
|
}
|
|
162
221
|
return /*#__PURE__*/React.createElement(Text, {
|
|
163
222
|
color: oneHunter.textAlt,
|
|
164
223
|
dimColor: true
|
|
165
|
-
}, '
|
|
224
|
+
}, ' ', "[", node.shortcut, "]");
|
|
166
225
|
};
|
|
167
|
-
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, {
|
|
168
229
|
color: oneHunter.textAlt
|
|
169
|
-
},
|
|
170
|
-
color:
|
|
171
|
-
},
|
|
230
|
+
}, hasChildren && !isCollapsed ? '▼ ' : '▶ '), !isRoot && !isDirectory && /*#__PURE__*/React.createElement(Text, {
|
|
231
|
+
color: oneHunter.textAlt
|
|
232
|
+
}, "\u2500 "), /*#__PURE__*/React.createElement(Text, {
|
|
172
233
|
bold: isSelected,
|
|
173
234
|
color: isSelected ? oneHunter.bg : fileColor,
|
|
174
235
|
backgroundColor: isSelected ? oneHunter.green : undefined
|
|
175
|
-
}, node.data.name), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
236
|
+
}, node.data.name, !node.data.isFile && !isRoot ? '/' : ''), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
176
237
|
color: oneHunter.textAlt
|
|
177
|
-
}, " (", node.children.length, ")"),
|
|
238
|
+
}, " (", node.children.length, ")"), isLoading ? /*#__PURE__*/React.createElement(Text, {
|
|
239
|
+
color: oneHunter.yellow
|
|
240
|
+
}, " ", SPINNER_FRAMES[spinnerFrame]) : displaySize > 0 ? /*#__PURE__*/React.createElement(Text, {
|
|
178
241
|
color: oneHunter.cyan
|
|
179
|
-
}, " ", formatBytes(
|
|
180
|
-
|
|
242
|
+
}, " ", formatBytes(displaySize)) : /*#__PURE__*/React.createElement(Text, null, " "), renderShortcut(), lastModified && /*#__PURE__*/React.createElement(Text, {
|
|
243
|
+
color: oneHunter.text2
|
|
244
|
+
}, " ", formatDate(lastModified))));
|
|
245
|
+
});
|
|
181
246
|
const TreeVisualization = ({
|
|
182
247
|
dirPath = '.',
|
|
183
248
|
maxLevel = 1,
|
|
184
|
-
noSize =
|
|
249
|
+
noSize = true,
|
|
250
|
+
collapseAll = false
|
|
185
251
|
}) => {
|
|
186
252
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
187
253
|
const [collapsed, setCollapsed] = useState(new Set());
|
|
188
254
|
const [shortcutInput, setShortcutInput] = useState('');
|
|
189
255
|
const [showTreeMap, setShowTreeMap] = useState(false);
|
|
190
256
|
const [initialized, setInitialized] = useState(false);
|
|
257
|
+
const [calculatedSizes, setCalculatedSizes] = useState(new Map());
|
|
258
|
+
const [loadingPaths, setLoadingPaths] = useState(new Set());
|
|
259
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
260
|
+
const [rootTree, setRootTree] = useState(() => hierarchy(readDirTree(dirPath, noSize, 0, maxLevel)));
|
|
261
|
+
const [lastModifiedDates, setLastModifiedDates] = useState(new Map());
|
|
191
262
|
const {
|
|
192
263
|
stdout
|
|
193
264
|
} = useStdout();
|
|
194
265
|
const terminalHeight = stdout?.rows || 24;
|
|
195
266
|
const viewportHeight = Math.max(5, terminalHeight - 7);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
267
|
+
|
|
268
|
+
// Spinner animation loop
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
const interval = setInterval(() => setSpinnerFrame(f => (f + 1) % SPINNER_FRAMES.length), 80);
|
|
271
|
+
return () => clearInterval(interval);
|
|
272
|
+
}, []);
|
|
273
|
+
|
|
274
|
+
// Preload all sizes when -s flag is used (noSize is false)
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (noSize) return; // Only preload when sizes are requested
|
|
277
|
+
|
|
278
|
+
const preloadAllSizes = async () => {
|
|
279
|
+
const newMap = new Map();
|
|
280
|
+
const nodesToCalculate = [];
|
|
281
|
+
|
|
282
|
+
// Collect all nodes
|
|
283
|
+
const traverse = node => {
|
|
284
|
+
nodesToCalculate.push(node);
|
|
285
|
+
if (node.children) {
|
|
286
|
+
node.children.forEach(traverse);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
traverse(rootTree);
|
|
290
|
+
|
|
291
|
+
// Set all nodes as loading
|
|
292
|
+
setLoadingPaths(new Set(nodesToCalculate.map(n => n.data.path)));
|
|
293
|
+
|
|
294
|
+
// Calculate sizes for all nodes with streaming updates
|
|
295
|
+
for (const node of nodesToCalculate) {
|
|
296
|
+
const sz = node.data.isFile ? fs.statSync(node.data.path).size : await getDirSizeAsync(node.data.path);
|
|
297
|
+
newMap.set(node.data.path, sz);
|
|
298
|
+
|
|
299
|
+
// Stream update immediately (using startTransition to deprioritize render)
|
|
300
|
+
React.startTransition(() => {
|
|
301
|
+
setCalculatedSizes(new Map(newMap));
|
|
302
|
+
setLoadingPaths(prev => {
|
|
303
|
+
const next = new Set(prev);
|
|
304
|
+
next.delete(node.data.path);
|
|
305
|
+
return next;
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
await new Promise(r => setTimeout(r, 0));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
preloadAllSizes();
|
|
312
|
+
}, [noSize, rootTree]);
|
|
201
313
|
useEffect(() => {
|
|
314
|
+
// Only initialize collapse state on first mount, not on every rootTree change
|
|
315
|
+
if (initialized) return;
|
|
202
316
|
const newCollapsed = new Set();
|
|
203
317
|
const applyLevel = (node, level = 0) => {
|
|
204
318
|
if (node.children) {
|
|
205
|
-
if (level >= maxLevel) newCollapsed.add(node.data.path);
|
|
206
|
-
node.children.forEach(
|
|
319
|
+
if (collapseAll && level >= 1 || level >= maxLevel) newCollapsed.add(node.data.path);
|
|
320
|
+
node.children.forEach(c => applyLevel(c, level + 1));
|
|
207
321
|
}
|
|
208
322
|
};
|
|
209
|
-
applyLevel(
|
|
323
|
+
applyLevel(rootTree);
|
|
210
324
|
setCollapsed(newCollapsed);
|
|
211
325
|
setInitialized(true);
|
|
212
|
-
}, [
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
node.children.forEach((child, i) => assignShortcuts(child, node.shortcut, i));
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
const cloned = root.copy();
|
|
222
|
-
assignShortcuts(cloned);
|
|
223
|
-
return cloned;
|
|
224
|
-
}, [root]);
|
|
326
|
+
}, []);
|
|
327
|
+
const assignShortcuts = (node, parentShortcut = '', siblingIndex = 0) => {
|
|
328
|
+
node.shortcut = generateShortcut(siblingIndex, node.data.isFile, parentShortcut);
|
|
329
|
+
if (node.children) node.children.forEach((child, i) => assignShortcuts(child, node.shortcut, i));
|
|
330
|
+
};
|
|
331
|
+
assignShortcuts(rootTree);
|
|
225
332
|
const visibleNodes = useMemo(() => {
|
|
226
333
|
const nodes = [];
|
|
227
334
|
const traverse = node => {
|
|
228
335
|
nodes.push(node);
|
|
229
|
-
if (node.children && !collapsed.has(node.data.path))
|
|
230
|
-
node.children.forEach(traverse);
|
|
231
|
-
}
|
|
336
|
+
if (node.children && !collapsed.has(node.data.path)) node.children.forEach(traverse);
|
|
232
337
|
};
|
|
233
|
-
traverse(
|
|
338
|
+
traverse(rootTree);
|
|
234
339
|
return nodes;
|
|
235
|
-
}, [
|
|
340
|
+
}, [rootTree, collapsed]);
|
|
236
341
|
const shortcutMap = useMemo(() => {
|
|
237
342
|
const map = new Map();
|
|
238
343
|
visibleNodes.forEach((node, index) => map.set(node.shortcut, index));
|
|
239
344
|
return map;
|
|
240
345
|
}, [visibleNodes]);
|
|
241
|
-
|
|
346
|
+
const calculateSizeAsync = async node => {
|
|
347
|
+
// Collect node and all its descendants (not just visible ones)
|
|
348
|
+
const nodesToCalculate = [];
|
|
349
|
+
const collectNodes = n => {
|
|
350
|
+
// Skip if already calculated
|
|
351
|
+
if (calculatedSizes.has(n.data.path)) {
|
|
352
|
+
// Still recurse to children in case they need calculation
|
|
353
|
+
if (n.children) {
|
|
354
|
+
n.children.forEach(collectNodes);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
nodesToCalculate.push(n);
|
|
359
|
+
if (n.children) {
|
|
360
|
+
n.children.forEach(collectNodes);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
collectNodes(node);
|
|
364
|
+
if (nodesToCalculate.length === 0) return; // Nothing to calculate
|
|
365
|
+
|
|
366
|
+
// Mark all nodes as loading
|
|
367
|
+
const pathsToLoad = nodesToCalculate.map(n => n.data.path);
|
|
368
|
+
setLoadingPaths(prev => new Set([...prev, ...pathsToLoad]));
|
|
369
|
+
const newMap = new Map(calculatedSizes);
|
|
370
|
+
|
|
371
|
+
// Calculate and stream updates for each node individually
|
|
372
|
+
for (const targetNode of nodesToCalculate) {
|
|
373
|
+
const sz = targetNode.data.isFile ? fs.statSync(targetNode.data.path).size : await getDirSizeAsync(targetNode.data.path);
|
|
374
|
+
newMap.set(targetNode.data.path, sz);
|
|
375
|
+
|
|
376
|
+
// Stream the update immediately (using startTransition to deprioritize render)
|
|
377
|
+
React.startTransition(() => {
|
|
378
|
+
setCalculatedSizes(new Map(newMap));
|
|
379
|
+
setLoadingPaths(prev => {
|
|
380
|
+
const next = new Set(prev);
|
|
381
|
+
next.delete(targetNode.data.path);
|
|
382
|
+
return next;
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
await new Promise(r => setTimeout(r, 0));
|
|
386
|
+
}
|
|
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
|
+
};
|
|
426
|
+
const traverseDir = async node => {
|
|
427
|
+
if (node.data.isFile) return;
|
|
428
|
+
|
|
429
|
+
// If children are undefined (not loaded yet), load them
|
|
430
|
+
if (node.data.children === undefined) {
|
|
431
|
+
// Load the children
|
|
432
|
+
const newTree = readDirTree(node.data.path, true, 0, 1);
|
|
433
|
+
if (newTree && newTree.children) {
|
|
434
|
+
node.data.children = newTree.children;
|
|
435
|
+
// Update both states - remove from collapsed and rebuild tree
|
|
436
|
+
setCollapsed(prev => {
|
|
437
|
+
const next = new Set(prev);
|
|
438
|
+
next.delete(node.data.path);
|
|
439
|
+
return next;
|
|
440
|
+
});
|
|
441
|
+
const newRootData = rootTree.data;
|
|
442
|
+
setRootTree(hierarchy(newRootData));
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// Children already loaded, just toggle expand
|
|
446
|
+
setCollapsed(prev => {
|
|
447
|
+
const next = new Set(prev);
|
|
448
|
+
if (next.has(node.data.path)) {
|
|
449
|
+
next.delete(node.data.path);
|
|
450
|
+
} else {
|
|
451
|
+
next.add(node.data.path);
|
|
452
|
+
}
|
|
453
|
+
return next;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
useInput(async (input, key) => {
|
|
458
|
+
if (showTreeMap) {
|
|
459
|
+
setShowTreeMap(false);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
242
462
|
if (input === 'q') process.exit(0);
|
|
243
|
-
if (input === '
|
|
244
|
-
|
|
463
|
+
if (input === 'C') {
|
|
464
|
+
// Toggle collapse/expand all children of the selected node's PARENT
|
|
465
|
+
const selectedNode = visibleNodes[selectedIndex];
|
|
466
|
+
if (!selectedNode || !selectedNode.parent) return;
|
|
467
|
+
const parentNode = selectedNode.parent;
|
|
468
|
+
|
|
469
|
+
// Get all descendant nodes with children
|
|
470
|
+
const descendants = [];
|
|
471
|
+
const collectDescendants = node => {
|
|
472
|
+
if (node.children && node.children.length > 0) {
|
|
473
|
+
descendants.push(node);
|
|
474
|
+
node.children.forEach(collectDescendants);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
collectDescendants(parentNode);
|
|
478
|
+
if (descendants.length === 0) return; // No children to collapse
|
|
479
|
+
|
|
480
|
+
// Check if all descendants are collapsed
|
|
481
|
+
const allCollapsed = descendants.every(n => collapsed.has(n.data.path));
|
|
482
|
+
setCollapsed(prev => {
|
|
483
|
+
const next = new Set(prev);
|
|
484
|
+
descendants.forEach(n => {
|
|
485
|
+
if (allCollapsed) {
|
|
486
|
+
next.delete(n.data.path);
|
|
487
|
+
} else {
|
|
488
|
+
next.add(n.data.path);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return next;
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (input === 'O') {
|
|
496
|
+
// Toggle collapse/expand ALL nodes in the entire tree
|
|
497
|
+
const allNodesWithChildren = [];
|
|
498
|
+
const collectAll = node => {
|
|
499
|
+
if (node.children && node.children.length > 0) {
|
|
500
|
+
allNodesWithChildren.push(node);
|
|
501
|
+
node.children.forEach(collectAll);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
collectAll(rootTree);
|
|
505
|
+
if (allNodesWithChildren.length === 0) return;
|
|
506
|
+
|
|
507
|
+
// Check if all nodes are collapsed
|
|
508
|
+
const allCollapsed = allNodesWithChildren.every(n => collapsed.has(n.data.path));
|
|
509
|
+
setCollapsed(prev => {
|
|
510
|
+
const next = new Set(prev);
|
|
511
|
+
allNodesWithChildren.forEach(n => {
|
|
512
|
+
if (allCollapsed) {
|
|
513
|
+
next.delete(n.data.path);
|
|
514
|
+
} else {
|
|
515
|
+
next.add(n.data.path);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return next;
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (input === 'm') {
|
|
523
|
+
const node = visibleNodes[selectedIndex];
|
|
524
|
+
if (!node) return;
|
|
525
|
+
// Always start calculating sizes in the background (calculateSizeAsync skips already-calculated nodes)
|
|
526
|
+
calculateSizeAsync(node);
|
|
527
|
+
// Show treemap immediately
|
|
528
|
+
setShowTreeMap(true);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (input === 'd') {
|
|
532
|
+
const node = visibleNodes[selectedIndex];
|
|
533
|
+
if (node) await calculateSizeAsync(node);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (input === 'l') {
|
|
537
|
+
const node = visibleNodes[selectedIndex];
|
|
538
|
+
if (node) await getLastModifiedAsync(node);
|
|
245
539
|
return;
|
|
246
540
|
}
|
|
247
|
-
if (showTreeMap) return;
|
|
248
541
|
if (key.upArrow) {
|
|
249
542
|
setSelectedIndex(p => Math.max(0, p - 1));
|
|
250
543
|
setShortcutInput('');
|
|
251
544
|
return;
|
|
252
|
-
}
|
|
545
|
+
}
|
|
546
|
+
if (key.downArrow) {
|
|
253
547
|
setSelectedIndex(p => Math.min(visibleNodes.length - 1, p + 1));
|
|
254
548
|
setShortcutInput('');
|
|
255
549
|
return;
|
|
256
550
|
}
|
|
257
551
|
if (key.return) {
|
|
258
|
-
if (shortcutInput && shortcutMap.has(shortcutInput)) {
|
|
259
|
-
const shortcutIndex = shortcutMap.get(shortcutInput);
|
|
260
|
-
const shortcutNode = visibleNodes[shortcutIndex];
|
|
261
|
-
setSelectedIndex(shortcutIndex);
|
|
262
|
-
if (shortcutNode.children) {
|
|
263
|
-
setCollapsed(prev => {
|
|
264
|
-
const next = new Set(prev);
|
|
265
|
-
if (next.has(shortcutNode.data.path)) next.delete(shortcutNode.data.path);else next.add(shortcutNode.data.path);
|
|
266
|
-
return next;
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
setShortcutInput('');
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
552
|
const node = visibleNodes[selectedIndex];
|
|
273
|
-
if (node
|
|
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 if (node.children) {
|
|
563
|
+
// Children already loaded, toggle collapse
|
|
274
564
|
setCollapsed(prev => {
|
|
275
565
|
const next = new Set(prev);
|
|
276
566
|
if (next.has(node.data.path)) next.delete(node.data.path);else next.add(node.data.path);
|
|
@@ -287,80 +577,99 @@ const TreeVisualization = ({
|
|
|
287
577
|
if (input && /^[a-z0-9]$/.test(input)) {
|
|
288
578
|
const newInput = shortcutInput + input;
|
|
289
579
|
setShortcutInput(newInput);
|
|
290
|
-
if (shortcutMap.has(newInput))
|
|
291
|
-
setSelectedIndex(shortcutMap.get(newInput));
|
|
292
|
-
}
|
|
580
|
+
if (shortcutMap.has(newInput)) setSelectedIndex(shortcutMap.get(newInput));
|
|
293
581
|
}
|
|
294
582
|
});
|
|
295
583
|
if (showTreeMap) {
|
|
296
584
|
const node = visibleNodes[selectedIndex];
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
585
|
+
|
|
586
|
+
// Populate value property on DATA objects for treemap visualization
|
|
587
|
+
// This is important because TreeMapView creates a new hierarchy from node.data,
|
|
588
|
+
// so we need to set values on the data objects, not the hierarchy nodes
|
|
589
|
+
const populateValues = hierarchyNode => {
|
|
590
|
+
if (!hierarchyNode) return;
|
|
591
|
+
|
|
592
|
+
// Recursively populate children first (depth-first, bottom-up)
|
|
593
|
+
if (hierarchyNode.children) {
|
|
594
|
+
hierarchyNode.children.forEach(populateValues);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Set value on the DATA object
|
|
598
|
+
const data = hierarchyNode.data;
|
|
599
|
+
if (data.isFile) {
|
|
600
|
+
// Files: use calculatedSize or read from disk
|
|
601
|
+
const calcSize = calculatedSizes.get(data.path);
|
|
602
|
+
if (calcSize !== undefined) {
|
|
603
|
+
data.value = calcSize;
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
data.value = fs.statSync(data.path).size;
|
|
607
|
+
} catch {
|
|
608
|
+
data.value = 0;
|
|
609
|
+
}
|
|
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
|
+
} else {
|
|
618
|
+
// For unloaded directories without calculated size, will show as 0
|
|
619
|
+
data.value = data.size || 0;
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
// Directory with loaded children - let d3's .sum() aggregate from children
|
|
623
|
+
data.value = undefined;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
populateValues(node);
|
|
300
627
|
return /*#__PURE__*/React.createElement(TreeMapView, {
|
|
301
628
|
selectedNode: node,
|
|
302
|
-
stdout: stdout
|
|
629
|
+
stdout: stdout,
|
|
630
|
+
calculatedSizes: calculatedSizes,
|
|
631
|
+
loadingPaths: loadingPaths
|
|
303
632
|
});
|
|
304
633
|
}
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
let end = start + viewportHeight;
|
|
308
|
-
if (end > visibleNodes.length) {
|
|
309
|
-
end = visibleNodes.length;
|
|
310
|
-
start = Math.max(0, end - viewportHeight);
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
start,
|
|
314
|
-
end
|
|
315
|
-
};
|
|
316
|
-
};
|
|
317
|
-
const {
|
|
318
|
-
start,
|
|
319
|
-
end
|
|
320
|
-
} = getViewportWindow();
|
|
634
|
+
const start = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
|
|
635
|
+
const end = Math.min(visibleNodes.length, start + viewportHeight);
|
|
321
636
|
const viewportNodes = visibleNodes.slice(start, end);
|
|
322
637
|
if (!initialized) return null;
|
|
323
638
|
return /*#__PURE__*/React.createElement(Box, {
|
|
324
639
|
flexDirection: "column"
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}, "\u2191/\u2193: Navigate | Enter: Toggle | t: TreeMap | Esc: Clear | q: Quit") : /*#__PURE__*/React.createElement(Text, {
|
|
640
|
+
}, /*#__PURE__*/React.createElement(Box, {
|
|
641
|
+
justifyContent: "space-between"
|
|
642
|
+
}, /*#__PURE__*/React.createElement(Text, {
|
|
329
643
|
color: oneHunter.textAlt,
|
|
330
644
|
dimColor: true
|
|
331
|
-
}, "\u2191/\u2193: Navigate | Enter:
|
|
645
|
+
}, "\u2191/\u2193: Navigate | Enter: Expand/Traverse | d: Size | l: Date | m: Map | q: Quit"), loadingPaths.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
332
646
|
color: oneHunter.yellow
|
|
333
|
-
},
|
|
647
|
+
}, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
|
|
334
648
|
key: start + i,
|
|
335
649
|
node: node,
|
|
336
650
|
isSelected: start + i === selectedIndex,
|
|
337
651
|
collapsed: collapsed,
|
|
338
652
|
shortcutInput: shortcutInput,
|
|
339
|
-
noSize: noSize
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
653
|
+
noSize: noSize,
|
|
654
|
+
calculatedSizes: calculatedSizes,
|
|
655
|
+
loadingPaths: loadingPaths,
|
|
656
|
+
spinnerFrame: spinnerFrame,
|
|
657
|
+
lastModifiedDates: lastModifiedDates
|
|
658
|
+
})));
|
|
343
659
|
};
|
|
344
660
|
|
|
345
|
-
// --- CLI
|
|
661
|
+
// --- CLI ---
|
|
346
662
|
const args = process.argv.slice(2);
|
|
347
|
-
|
|
348
|
-
// Help
|
|
349
663
|
if (args.includes('--help') || args.includes('-h')) {
|
|
350
664
|
console.log(`
|
|
351
665
|
Usage:
|
|
352
666
|
tri [directory] [options]
|
|
353
667
|
|
|
354
|
-
Examples:
|
|
355
|
-
tri . # visualize current directory
|
|
356
|
-
tri ../bionemo -L 2 # show 2 levels deep
|
|
357
|
-
tri --dir src # specify directory with flag
|
|
358
|
-
|
|
359
668
|
Options:
|
|
360
|
-
|
|
361
|
-
-
|
|
362
|
-
|
|
363
|
-
-h, --help Show
|
|
669
|
+
-L <level> Depth (default 1)
|
|
670
|
+
-s, --size Preload all sizes (default off)
|
|
671
|
+
-c, --collapse Start collapsed
|
|
672
|
+
-h, --help Show help
|
|
364
673
|
`);
|
|
365
674
|
process.exit(0);
|
|
366
675
|
}
|
|
@@ -369,9 +678,15 @@ const firstNonFlagArg = args.find(arg => !arg.startsWith('-'));
|
|
|
369
678
|
const dirPath = dirIndex !== -1 && args[dirIndex + 1] ? args[dirIndex + 1] : firstNonFlagArg || '.';
|
|
370
679
|
const levelIndex = args.indexOf('-L');
|
|
371
680
|
const maxLevel = levelIndex !== -1 && args[levelIndex + 1] ? parseInt(args[levelIndex + 1], 10) : 1;
|
|
372
|
-
|
|
681
|
+
if (isNaN(maxLevel) || maxLevel < 1) {
|
|
682
|
+
console.error('tri: Invalid level, must be greater than 0.');
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
const collapseAll = args.includes('--collapse') || args.includes('-c');
|
|
686
|
+
const preloadSizes = args.includes('--size') || args.includes('-s');
|
|
373
687
|
render(/*#__PURE__*/React.createElement(TreeVisualization, {
|
|
374
688
|
dirPath: dirPath,
|
|
375
689
|
maxLevel: maxLevel,
|
|
376
|
-
noSize:
|
|
690
|
+
noSize: !preloadSizes,
|
|
691
|
+
collapseAll: collapseAll
|
|
377
692
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tri-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"description": "Interactive CLI directory tree visualizer with treemap view",
|
|
5
5
|
"author": "Jared Wilber",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
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"
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"extends": "xo-react",
|
|
66
66
|
"prettier": true,
|
|
67
67
|
"rules": {
|
|
68
|
-
"react/prop-types": "off"
|
|
68
|
+
"react/prop-types": "off",
|
|
69
|
+
"unicorn/expiring-todo-comments": "off"
|
|
69
70
|
}
|
|
70
71
|
},
|
|
71
72
|
"prettier": "@vdemedes/prettier-config",
|
|
@@ -74,4 +75,4 @@
|
|
|
74
75
|
"@babel/preset-react"
|
|
75
76
|
]
|
|
76
77
|
}
|
|
77
|
-
}
|
|
78
|
+
}
|
package/readme.tape
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Output assets/readme.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 2s
|
|
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
|
+
Down
|
|
25
|
+
Sleep 250ms
|
|
26
|
+
Down
|
|
27
|
+
Sleep 250ms
|
|
28
|
+
Down
|
|
29
|
+
Sleep 250ms
|
|
30
|
+
Type "l"
|
|
31
|
+
Sleep 1.5s
|
|
32
|
+
|
|
33
|
+
Enter
|
|
34
|
+
Sleep 1.5s
|
|
35
|
+
Type "m"
|
|
36
|
+
Sleep 1.5s
|
|
37
|
+
Type "m"
|
|
38
|
+
Sleep 500ms
|
|
39
|
+
Down
|
|
40
|
+
Sleep 250ms
|
|
41
|
+
Down
|
|
42
|
+
Sleep 250ms
|
|
43
|
+
Enter
|
|
44
|
+
Sleep 1.5s
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Toggle treemap view
|
|
48
|
+
Type "m"
|
|
49
|
+
Sleep 2s
|
|
50
|
+
|
|
51
|
+
# Return to tree view
|
|
52
|
+
Type "m"
|
|
53
|
+
Sleep 2s
|
|
54
|
+
|
|
55
|
+
Type "a"
|
|
56
|
+
Sleep 250ms
|
|
57
|
+
Type "c"
|
|
58
|
+
Sleep 500ms
|
|
59
|
+
Enter
|
|
60
|
+
Sleep 1s
|
|
61
|
+
|
|
62
|
+
Type "a"
|
|
63
|
+
Sleep 250ms
|
|
64
|
+
Type "h"
|
|
65
|
+
Sleep 500ms
|
|
66
|
+
Enter
|
|
67
|
+
Sleep 1s
|
|
68
|
+
|
|
69
|
+
Enter
|
|
70
|
+
Sleep 1s
|
|
71
|
+
|
|
72
|
+
# Quit
|
|
73
|
+
Type "q"
|
|
74
|
+
Sleep 1s
|
|
75
|
+
|
|
76
|
+
Type "# install with: npm i -g tri-cli"
|
|
77
|
+
Enter
|
|
78
|
+
Sleep 2s
|