tri-cli 0.0.2 → 0.0.3
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 +51 -0
- package/dist/cli.js +54 -57
- package/package.json +1 -1
- package/readme.md +0 -25
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# tri-cli
|
|
2
|
+
|
|
3
|
+
A terminal-based interactive directory visualizer, like `tree` but with interactivity and treemaps:
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g tri-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
tri [directory] [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Examples
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
tri . # visualize current directory
|
|
21
|
+
tri ../bionemo -L 2 # show 2 levels deep
|
|
22
|
+
tri --dir src # specify directory with flag
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Interactive Controls
|
|
26
|
+
|
|
27
|
+
| Key | Action |
|
|
28
|
+
| ----- | ------------------------------ |
|
|
29
|
+
| ↑ / ↓ | Navigate files and folders |
|
|
30
|
+
| Enter | Expand or collapse a directory |
|
|
31
|
+
| t | Toggle Treemap view |
|
|
32
|
+
| Esc | Clear typed shortcut |
|
|
33
|
+
| q | Quit |
|
|
34
|
+
|
|
35
|
+
### Options
|
|
36
|
+
|
|
37
|
+
| Option | Description |
|
|
38
|
+
| -------------- | ----------------------------------------------- |
|
|
39
|
+
| `--dir <path>` | Directory to visualize (optional if positional) |
|
|
40
|
+
| `-L <level>` | Depth level to expand initially (default: 1) |
|
|
41
|
+
| `-h, --help` | Show help message |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
**Example:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
tri ../bionemo -L 2
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Type `t` to open (and close) the treemap view, navigate with arrow keys, and press `q` to quit.
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,10 @@ import { TreeMapView } from './Treemap.js';
|
|
|
8
8
|
const oneHunter = {
|
|
9
9
|
bg: '#282c34',
|
|
10
10
|
bgAlt: '#21252b',
|
|
11
|
-
text: '
|
|
11
|
+
text: 'rgb(206, 205, 195)',
|
|
12
|
+
text2: 'rgb(135, 133, 128)',
|
|
12
13
|
textAlt: 'white',
|
|
13
|
-
red: '
|
|
14
|
+
red: 'rgb(209, 77, 65)',
|
|
14
15
|
orange: 'rgb(218, 112, 44)',
|
|
15
16
|
yellow: '#e5c07b',
|
|
16
17
|
green: '#98c379',
|
|
@@ -20,7 +21,7 @@ const oneHunter = {
|
|
|
20
21
|
magenta: 'rgb(206, 93, 151)'
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// --- Helpers ---
|
|
24
25
|
const formatBytes = bytes => {
|
|
25
26
|
if (bytes === 0) return '0 B';
|
|
26
27
|
const k = 1024;
|
|
@@ -28,8 +29,6 @@ const formatBytes = bytes => {
|
|
|
28
29
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
29
30
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
30
31
|
};
|
|
31
|
-
|
|
32
|
-
// Function to get directory size recursively
|
|
33
32
|
const getDirSize = dirPath => {
|
|
34
33
|
let size = 0;
|
|
35
34
|
try {
|
|
@@ -49,13 +48,13 @@ const getDirSize = dirPath => {
|
|
|
49
48
|
return size;
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
//
|
|
53
|
-
const readDirTree = dirPath => {
|
|
51
|
+
// ✅ Updated: allow skipping size computation
|
|
52
|
+
const readDirTree = (dirPath, noSize = false) => {
|
|
54
53
|
try {
|
|
55
54
|
const stats = fs.statSync(dirPath);
|
|
56
55
|
const name = path.basename(dirPath) || dirPath;
|
|
57
56
|
if (!stats.isDirectory()) {
|
|
58
|
-
const size = stats.size;
|
|
57
|
+
const size = noSize ? 0 : stats.size;
|
|
59
58
|
return {
|
|
60
59
|
name,
|
|
61
60
|
path: dirPath,
|
|
@@ -64,21 +63,21 @@ const readDirTree = dirPath => {
|
|
|
64
63
|
value: size > 0 ? size : 100
|
|
65
64
|
};
|
|
66
65
|
}
|
|
67
|
-
const children = fs.readdirSync(dirPath).filter(file => !file.startsWith('.')).map(file => readDirTree(path.join(dirPath, file))).filter(Boolean);
|
|
66
|
+
const children = fs.readdirSync(dirPath).filter(file => !file.startsWith('.')).map(file => readDirTree(path.join(dirPath, file), noSize)).filter(Boolean);
|
|
68
67
|
return {
|
|
69
68
|
name,
|
|
70
69
|
path: dirPath,
|
|
71
70
|
isFile: false,
|
|
72
|
-
size: getDirSize(dirPath),
|
|
71
|
+
size: noSize ? 0 : getDirSize(dirPath),
|
|
73
72
|
children: children.length > 0 ? children : undefined
|
|
74
73
|
};
|
|
75
74
|
} catch {
|
|
76
75
|
return null;
|
|
77
76
|
}
|
|
78
77
|
};
|
|
79
|
-
|
|
80
|
-
// Get color based on file extension
|
|
81
78
|
const getFileColor = filename => {
|
|
79
|
+
if (filename === 'VERSION') return oneHunter.purple;
|
|
80
|
+
if (filename === 'Dockerfile') return oneHunter.blue;
|
|
82
81
|
const ext = path.extname(filename).toLowerCase();
|
|
83
82
|
const colorMap = {
|
|
84
83
|
'.js': oneHunter.yellow,
|
|
@@ -90,7 +89,7 @@ const getFileColor = filename => {
|
|
|
90
89
|
'.json': oneHunter.purple,
|
|
91
90
|
'.html': oneHunter.cyan,
|
|
92
91
|
'.css': oneHunter.cyan,
|
|
93
|
-
'.md': oneHunter.
|
|
92
|
+
'.md': oneHunter.orange,
|
|
94
93
|
'.txt': oneHunter.textAlt,
|
|
95
94
|
'.sh': oneHunter.green,
|
|
96
95
|
'.yml': oneHunter.purple,
|
|
@@ -100,12 +99,11 @@ const getFileColor = filename => {
|
|
|
100
99
|
'.png': oneHunter.blue,
|
|
101
100
|
'.jpg': oneHunter.blue,
|
|
102
101
|
'.gif': oneHunter.blue,
|
|
103
|
-
'.pdf': oneHunter.red
|
|
102
|
+
'.pdf': oneHunter.red,
|
|
103
|
+
'.toml': oneHunter.text2
|
|
104
104
|
};
|
|
105
105
|
return colorMap[ext] || oneHunter.text;
|
|
106
106
|
};
|
|
107
|
-
|
|
108
|
-
// Generate shortcut label
|
|
109
107
|
const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
110
108
|
const letters = 'abcdefghijklmnoprsuvwxyz'; // omitting q, t
|
|
111
109
|
const base = letters.length;
|
|
@@ -119,12 +117,13 @@ const generateShortcut = (index, isFile, parentShortcut = '') => {
|
|
|
119
117
|
return parentShortcut + label;
|
|
120
118
|
};
|
|
121
119
|
|
|
122
|
-
//
|
|
120
|
+
// --- Components ---
|
|
123
121
|
const TreeRow = /*#__PURE__*/React.memo(({
|
|
124
122
|
node,
|
|
125
123
|
isSelected,
|
|
126
124
|
collapsed,
|
|
127
|
-
shortcutInput
|
|
125
|
+
shortcutInput,
|
|
126
|
+
noSize
|
|
128
127
|
}) => {
|
|
129
128
|
const isCollapsed = collapsed.has(node.data.path);
|
|
130
129
|
const hasChildren = node.children && node.children.length > 0;
|
|
@@ -175,13 +174,14 @@ const TreeRow = /*#__PURE__*/React.memo(({
|
|
|
175
174
|
backgroundColor: isSelected ? oneHunter.green : undefined
|
|
176
175
|
}, node.data.name), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
|
|
177
176
|
color: oneHunter.textAlt
|
|
178
|
-
}, " (", node.children.length, ")"), node.data.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
179
|
-
color: oneHunter.
|
|
177
|
+
}, " (", node.children.length, ")"), !noSize && node.data.size > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
178
|
+
color: oneHunter.cyan
|
|
180
179
|
}, " ", formatBytes(node.data.size)), renderShortcut()));
|
|
181
|
-
}, (p, n) => p.isSelected === n.isSelected && p.collapsed === n.collapsed && p.shortcutInput === n.shortcutInput);
|
|
180
|
+
}, (p, n) => p.isSelected === n.isSelected && p.collapsed === n.collapsed && p.shortcutInput === n.shortcutInput && p.noSize === n.noSize);
|
|
182
181
|
const TreeVisualization = ({
|
|
183
182
|
dirPath = '.',
|
|
184
|
-
maxLevel = 1
|
|
183
|
+
maxLevel = 1,
|
|
184
|
+
noSize = false
|
|
185
185
|
}) => {
|
|
186
186
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
187
187
|
const [collapsed, setCollapsed] = useState(new Set());
|
|
@@ -193,13 +193,11 @@ const TreeVisualization = ({
|
|
|
193
193
|
} = useStdout();
|
|
194
194
|
const terminalHeight = stdout?.rows || 24;
|
|
195
195
|
const viewportHeight = Math.max(5, terminalHeight - 7);
|
|
196
|
-
const treeData = useMemo(() => readDirTree(dirPath), [dirPath]);
|
|
196
|
+
const treeData = useMemo(() => readDirTree(dirPath, noSize), [dirPath, noSize]);
|
|
197
197
|
if (!treeData) return /*#__PURE__*/React.createElement(Text, {
|
|
198
198
|
color: "red"
|
|
199
199
|
}, "Error: Could not read directory");
|
|
200
200
|
const root = useMemo(() => hierarchy(treeData), [treeData]);
|
|
201
|
-
|
|
202
|
-
// --- Initial collapse based on -L ---
|
|
203
201
|
useEffect(() => {
|
|
204
202
|
const newCollapsed = new Set();
|
|
205
203
|
const applyLevel = (node, level = 0) => {
|
|
@@ -212,8 +210,6 @@ const TreeVisualization = ({
|
|
|
212
210
|
setCollapsed(newCollapsed);
|
|
213
211
|
setInitialized(true);
|
|
214
212
|
}, [maxLevel, root]);
|
|
215
|
-
|
|
216
|
-
// Assign shortcuts
|
|
217
213
|
const rootWithShortcuts = useMemo(() => {
|
|
218
214
|
const assignShortcuts = (node, parentShortcut = '', siblingIndex = 0) => {
|
|
219
215
|
const isFile = node.data.isFile;
|
|
@@ -226,8 +222,6 @@ const TreeVisualization = ({
|
|
|
226
222
|
assignShortcuts(cloned);
|
|
227
223
|
return cloned;
|
|
228
224
|
}, [root]);
|
|
229
|
-
|
|
230
|
-
// Flatten visible nodes
|
|
231
225
|
const visibleNodes = useMemo(() => {
|
|
232
226
|
const nodes = [];
|
|
233
227
|
const traverse = node => {
|
|
@@ -247,7 +241,7 @@ const TreeVisualization = ({
|
|
|
247
241
|
useInput((input, key) => {
|
|
248
242
|
if (input === 'q') process.exit(0);
|
|
249
243
|
if (input === 't') {
|
|
250
|
-
setShowTreeMap(p => !p);
|
|
244
|
+
if (!noSize) setShowTreeMap(p => !p);
|
|
251
245
|
return;
|
|
252
246
|
}
|
|
253
247
|
if (showTreeMap) return;
|
|
@@ -260,14 +254,10 @@ const TreeVisualization = ({
|
|
|
260
254
|
setShortcutInput('');
|
|
261
255
|
return;
|
|
262
256
|
}
|
|
263
|
-
|
|
264
|
-
// ✅ ENTER behavior
|
|
265
257
|
if (key.return) {
|
|
266
258
|
if (shortcutInput && shortcutMap.has(shortcutInput)) {
|
|
267
259
|
const shortcutIndex = shortcutMap.get(shortcutInput);
|
|
268
260
|
const shortcutNode = visibleNodes[shortcutIndex];
|
|
269
|
-
|
|
270
|
-
// Jump and toggle collapse (in one go)
|
|
271
261
|
setSelectedIndex(shortcutIndex);
|
|
272
262
|
if (shortcutNode.children) {
|
|
273
263
|
setCollapsed(prev => {
|
|
@@ -279,8 +269,6 @@ const TreeVisualization = ({
|
|
|
279
269
|
setShortcutInput('');
|
|
280
270
|
return;
|
|
281
271
|
}
|
|
282
|
-
|
|
283
|
-
// Otherwise toggle current node
|
|
284
272
|
const node = visibleNodes[selectedIndex];
|
|
285
273
|
if (node.children) {
|
|
286
274
|
setCollapsed(prev => {
|
|
@@ -296,13 +284,9 @@ const TreeVisualization = ({
|
|
|
296
284
|
setShortcutInput('');
|
|
297
285
|
return;
|
|
298
286
|
}
|
|
299
|
-
|
|
300
|
-
// Typing a shortcut
|
|
301
287
|
if (input && /^[a-z0-9]$/.test(input)) {
|
|
302
288
|
const newInput = shortcutInput + input;
|
|
303
289
|
setShortcutInput(newInput);
|
|
304
|
-
|
|
305
|
-
// Move highlight immediately
|
|
306
290
|
if (shortcutMap.has(newInput)) {
|
|
307
291
|
setSelectedIndex(shortcutMap.get(newInput));
|
|
308
292
|
}
|
|
@@ -318,8 +302,6 @@ const TreeVisualization = ({
|
|
|
318
302
|
stdout: stdout
|
|
319
303
|
});
|
|
320
304
|
}
|
|
321
|
-
|
|
322
|
-
// --- Viewport logic ---
|
|
323
305
|
const getViewportWindow = () => {
|
|
324
306
|
let start = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
|
|
325
307
|
let end = start + viewportHeight;
|
|
@@ -340,17 +322,21 @@ const TreeVisualization = ({
|
|
|
340
322
|
if (!initialized) return null;
|
|
341
323
|
return /*#__PURE__*/React.createElement(Box, {
|
|
342
324
|
flexDirection: "column"
|
|
343
|
-
}, /*#__PURE__*/React.createElement(Text, {
|
|
325
|
+
}, !noSize ? /*#__PURE__*/React.createElement(Text, {
|
|
344
326
|
color: oneHunter.textAlt,
|
|
345
327
|
dimColor: true
|
|
346
|
-
}, "\u2191/\u2193: Navigate | Enter: Toggle | t: TreeMap | Esc: Clear | q: Quit")
|
|
328
|
+
}, "\u2191/\u2193: Navigate | Enter: Toggle | t: TreeMap | Esc: Clear | q: Quit") : /*#__PURE__*/React.createElement(Text, {
|
|
329
|
+
color: oneHunter.textAlt,
|
|
330
|
+
dimColor: true
|
|
331
|
+
}, "\u2191/\u2193: Navigate | Enter: Toggle | Esc: Clear | q: Quit"), start > 0 && /*#__PURE__*/React.createElement(Text, {
|
|
347
332
|
color: oneHunter.yellow
|
|
348
333
|
}, "\u22EE (", start, " more above)"), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
|
|
349
334
|
key: start + i,
|
|
350
335
|
node: node,
|
|
351
336
|
isSelected: start + i === selectedIndex,
|
|
352
337
|
collapsed: collapsed,
|
|
353
|
-
shortcutInput: shortcutInput
|
|
338
|
+
shortcutInput: shortcutInput,
|
|
339
|
+
noSize: noSize
|
|
354
340
|
})), end < visibleNodes.length && /*#__PURE__*/React.createElement(Text, {
|
|
355
341
|
color: oneHunter.yellow
|
|
356
342
|
}, "\u22EE (", visibleNodes.length - end, " more below)"));
|
|
@@ -359,22 +345,33 @@ const TreeVisualization = ({
|
|
|
359
345
|
// --- CLI args ---
|
|
360
346
|
const args = process.argv.slice(2);
|
|
361
347
|
|
|
362
|
-
//
|
|
348
|
+
// Help
|
|
349
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
350
|
+
console.log(`
|
|
351
|
+
Usage:
|
|
352
|
+
tri [directory] [options]
|
|
353
|
+
|
|
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
|
+
Options:
|
|
360
|
+
--dir <path> Directory to visualize (optional if positional)
|
|
361
|
+
-L <level> Depth level to expand initially (default: 1)
|
|
362
|
+
--no-size, -ns Skip file size calculation (faster, disables treemap)
|
|
363
|
+
-h, --help Show this help message
|
|
364
|
+
`);
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
363
367
|
const dirIndex = args.indexOf('--dir');
|
|
364
|
-
let dirPath;
|
|
365
368
|
const firstNonFlagArg = args.find(arg => !arg.startsWith('-'));
|
|
366
|
-
|
|
367
|
-
dirPath = args[dirIndex + 1];
|
|
368
|
-
} else if (firstNonFlagArg) {
|
|
369
|
-
dirPath = firstNonFlagArg;
|
|
370
|
-
} else {
|
|
371
|
-
dirPath = '.';
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Handle -L flag
|
|
369
|
+
const dirPath = dirIndex !== -1 && args[dirIndex + 1] ? args[dirIndex + 1] : firstNonFlagArg || '.';
|
|
375
370
|
const levelIndex = args.indexOf('-L');
|
|
376
371
|
const maxLevel = levelIndex !== -1 && args[levelIndex + 1] ? parseInt(args[levelIndex + 1], 10) : 1;
|
|
372
|
+
const noSize = args.includes('--no-size') || args.includes('-ns');
|
|
377
373
|
render(/*#__PURE__*/React.createElement(TreeVisualization, {
|
|
378
374
|
dirPath: dirPath,
|
|
379
|
-
maxLevel: maxLevel
|
|
375
|
+
maxLevel: maxLevel,
|
|
376
|
+
noSize: noSize
|
|
380
377
|
}));
|
package/package.json
CHANGED
package/readme.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# tri
|
|
2
|
-
|
|
3
|
-
> This readme is automatically generated by [create-ink-app](https://github.com/vadimdemedes/create-ink-app)
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
$ npm install --global tri
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## CLI
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
$ tri --help
|
|
15
|
-
|
|
16
|
-
Usage
|
|
17
|
-
$ tri
|
|
18
|
-
|
|
19
|
-
Options
|
|
20
|
-
--name Your name
|
|
21
|
-
|
|
22
|
-
Examples
|
|
23
|
-
$ tri --name=Jane
|
|
24
|
-
Hello, Jane
|
|
25
|
-
```
|