tri-cli 0.0.2

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.
@@ -0,0 +1,393 @@
1
+ import React from 'react';
2
+ import { Text, Box } from 'ink';
3
+ import { hierarchy, treemap, treemapBinary } from 'd3-hierarchy';
4
+
5
+ // One Hunter color scheme
6
+ const oneHunter = {
7
+ bg: '#282c34',
8
+ bgAlt: '#21252b',
9
+ text: '#abb2bf',
10
+ textAlt: '#5c6370',
11
+ red: '#e06c75',
12
+ orange: '#d19a66',
13
+ yellow: '#e5c07b',
14
+ green: '#98c379',
15
+ cyan: '#56b6c2',
16
+ blue: '#61afef',
17
+ purple: '#c678dd',
18
+ magenta: '#c678dd'
19
+ };
20
+
21
+ // Expanded color palette for directories
22
+ const dirColors = [oneHunter.blue, oneHunter.cyan, oneHunter.green, oneHunter.yellow, oneHunter.orange, oneHunter.red, oneHunter.purple, oneHunter.magenta, '#4a9eff', '#7ec699', '#e8b339', '#ff7eb6'];
23
+
24
+ // Function to format bytes to human readable
25
+ const formatBytes = bytes => {
26
+ if (bytes === 0) return '0 B';
27
+ const k = 1024;
28
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
29
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
30
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
31
+ };
32
+
33
+ // Get color based on file extension
34
+ const getFileColor = filename => {
35
+ const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
36
+ const colorMap = {
37
+ '.js': oneHunter.yellow,
38
+ '.jsx': oneHunter.yellow,
39
+ '.ts': oneHunter.blue,
40
+ '.tsx': oneHunter.blue,
41
+ '.py': oneHunter.green,
42
+ '.rb': oneHunter.red,
43
+ '.json': oneHunter.purple,
44
+ '.html': oneHunter.cyan,
45
+ '.css': oneHunter.cyan,
46
+ '.md': oneHunter.text,
47
+ '.txt': oneHunter.textAlt,
48
+ '.sh': oneHunter.green,
49
+ '.yml': oneHunter.purple,
50
+ '.yaml': oneHunter.purple,
51
+ '.xml': oneHunter.orange,
52
+ '.svg': oneHunter.cyan,
53
+ '.png': oneHunter.blue,
54
+ '.jpg': oneHunter.blue,
55
+ '.gif': oneHunter.blue,
56
+ '.pdf': oneHunter.red
57
+ };
58
+ return colorMap[ext] || oneHunter.text;
59
+ };
60
+
61
+ // Simple hash function for consistent color assignment
62
+ const hashString = str => {
63
+ let hash = 0;
64
+ for (let i = 0; i < str.length; i++) {
65
+ const char = str.charCodeAt(i);
66
+ hash = (hash << 5) - hash + char;
67
+ hash = hash & hash;
68
+ }
69
+ return Math.abs(hash);
70
+ };
71
+
72
+ // Assign colors to directory groups
73
+ const assignGroupColors = root => {
74
+ const colorMap = new Map();
75
+ if (root.children) {
76
+ root.children.forEach(child => {
77
+ if (!child.data.isFile) {
78
+ const colorIndex = hashString(child.data.name) % dirColors.length;
79
+ colorMap.set(child.data.name, dirColors[colorIndex]);
80
+ }
81
+ });
82
+ }
83
+ root.descendants().forEach(node => {
84
+ if (!node.data.isFile && node !== root) {
85
+ let topAncestor = node;
86
+ while (topAncestor.parent && topAncestor.parent !== root) {
87
+ topAncestor = topAncestor.parent;
88
+ }
89
+ if (colorMap.has(topAncestor.data.name)) {
90
+ node.groupColor = colorMap.get(topAncestor.data.name);
91
+ } else {
92
+ const colorIndex = hashString(node.data.name) % dirColors.length;
93
+ node.groupColor = dirColors[colorIndex];
94
+ }
95
+ }
96
+ });
97
+ return colorMap;
98
+ };
99
+ export const TreeMapView = ({
100
+ selectedNode,
101
+ stdout
102
+ }) => {
103
+ if (!selectedNode || !selectedNode.data) {
104
+ return /*#__PURE__*/React.createElement(Box, {
105
+ flexDirection: "column"
106
+ }, /*#__PURE__*/React.createElement(Text, {
107
+ bold: true,
108
+ color: oneHunter.red
109
+ }, "Error: Invalid node selected"), /*#__PURE__*/React.createElement(Text, {
110
+ dimColor: true,
111
+ color: oneHunter.textAlt
112
+ }, "Press 't' to go back to tree view"));
113
+ }
114
+ const width = Math.min(stdout?.columns || 80, 120) - 4;
115
+ const height = Math.min(stdout?.rows || 24, 40) - 8;
116
+
117
+ // If it's a file, show just that file as a big rectangle
118
+ if (selectedNode.data.isFile) {
119
+ const color = getFileColor(selectedNode.data.name);
120
+ const name = selectedNode.data.name;
121
+ const sizeStr = selectedNode.data.size ? formatBytes(selectedNode.data.size) : '';
122
+ const grid = Array(height).fill(null).map(() => Array(width).fill(null).map(() => ({
123
+ char: ' ',
124
+ color
125
+ })));
126
+ for (let y = 0; y < height; y++) {
127
+ for (let x = 0; x < width; x++) {
128
+ const isTop = y === 0;
129
+ const isBottom = y === height - 1;
130
+ const isLeft = x === 0;
131
+ const isRight = x === width - 1;
132
+ let char = ' ';
133
+ if (isTop && isLeft) char = '┌';else if (isTop && isRight) char = '┐';else if (isBottom && isLeft) char = '└';else if (isBottom && isRight) char = '┘';else if (isTop || isBottom) char = '─';else if (isLeft || isRight) char = '│';else char = ' ';
134
+ grid[y][x] = {
135
+ char,
136
+ color
137
+ };
138
+ }
139
+ }
140
+ const midY = Math.floor(height / 2);
141
+ const nameX = Math.floor((width - name.length) / 2);
142
+ if (midY >= 2 && midY < height - 2) {
143
+ for (let i = 0; i < name.length && nameX + i < width - 2; i++) {
144
+ if (nameX + i > 0) {
145
+ grid[midY][nameX + i] = {
146
+ char: name[i],
147
+ color
148
+ };
149
+ }
150
+ }
151
+ if (sizeStr && midY + 2 < height - 2) {
152
+ const sizeX = Math.floor((width - sizeStr.length) / 2);
153
+ for (let i = 0; i < sizeStr.length && sizeX + i < width - 2; i++) {
154
+ if (sizeX + i > 0) {
155
+ grid[midY + 2][sizeX + i] = {
156
+ char: sizeStr[i],
157
+ color
158
+ };
159
+ }
160
+ }
161
+ }
162
+ }
163
+ const lines = grid.map((row, y) => {
164
+ const segments = [];
165
+ let currentColor = null;
166
+ let currentText = '';
167
+ row.forEach(cell => {
168
+ if (cell.color === currentColor) {
169
+ currentText += cell.char;
170
+ } else {
171
+ if (currentText) {
172
+ segments.push(/*#__PURE__*/React.createElement(Text, {
173
+ key: segments.length,
174
+ color: currentColor
175
+ }, currentText));
176
+ }
177
+ currentColor = cell.color;
178
+ currentText = cell.char;
179
+ }
180
+ });
181
+ if (currentText) {
182
+ segments.push(/*#__PURE__*/React.createElement(Text, {
183
+ key: segments.length,
184
+ color: currentColor
185
+ }, currentText));
186
+ }
187
+ return /*#__PURE__*/React.createElement(Box, {
188
+ key: y
189
+ }, segments);
190
+ });
191
+ return /*#__PURE__*/React.createElement(Box, {
192
+ flexDirection: "column"
193
+ }, /*#__PURE__*/React.createElement(Text, {
194
+ bold: true,
195
+ color: oneHunter.cyan
196
+ }, "TreeMap View: ", name, " (File)"), /*#__PURE__*/React.createElement(Text, {
197
+ dimColor: true,
198
+ color: oneHunter.textAlt
199
+ }, "Press 't' to toggle back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
200
+ borderStyle: "single",
201
+ borderColor: oneHunter.textAlt,
202
+ flexDirection: "column",
203
+ marginTop: 1
204
+ }, lines), /*#__PURE__*/React.createElement(Text, {
205
+ dimColor: true,
206
+ color: oneHunter.textAlt,
207
+ marginTop: 1
208
+ }, "File: ", name, " | Size: ", sizeStr || 'Unknown'));
209
+ }
210
+
211
+ // It's a directory - show treemap of its children
212
+ if (!selectedNode.children || selectedNode.children.length === 0) {
213
+ return /*#__PURE__*/React.createElement(Box, {
214
+ flexDirection: "column"
215
+ }, /*#__PURE__*/React.createElement(Text, {
216
+ bold: true,
217
+ color: oneHunter.cyan
218
+ }, "TreeMap View: ", selectedNode.data.name, " (Empty Directory)"), /*#__PURE__*/React.createElement(Text, {
219
+ dimColor: true,
220
+ color: oneHunter.textAlt
221
+ }, "Press 't' to toggle back to tree view | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
222
+ borderStyle: "single",
223
+ borderColor: oneHunter.textAlt,
224
+ marginTop: 1,
225
+ padding: 1
226
+ }, /*#__PURE__*/React.createElement(Text, {
227
+ color: oneHunter.textAlt
228
+ }, "This directory is empty or collapsed.")));
229
+ }
230
+ const root = hierarchy(selectedNode.data).sum(d => d.value || 0).sort((a, b) => (b.value || 0) - (a.value || 0));
231
+ assignGroupColors(root);
232
+
233
+ // Apply treemap layout
234
+ treemap().tile(treemapBinary).size([width, height]).paddingOuter(0.5).paddingTop(0.5).paddingInner(0.5).round(true)(root);
235
+
236
+ // Only show direct children (first level)
237
+ const firstLevelNodes = root.children || [];
238
+ const grid = Array(height).fill(null).map(() => Array(width).fill(null).map(() => ({
239
+ char: ' ',
240
+ color: oneHunter.textAlt
241
+ })));
242
+
243
+ // Draw each first-level child
244
+ firstLevelNodes.forEach(node => {
245
+ const x0 = Math.floor(node.x0);
246
+ const y0 = Math.floor(node.y0);
247
+ const x1 = Math.ceil(node.x1);
248
+ const y1 = Math.ceil(node.y1);
249
+ const isFile = node.data.isFile;
250
+ const boxWidth = x1 - x0;
251
+ const boxHeight = y1 - y0;
252
+ if (boxWidth < 1 || boxHeight < 1) return;
253
+
254
+ // Color based on type
255
+ const color = isFile ? getFileColor(node.data.name) : node.groupColor || oneHunter.blue;
256
+
257
+ // Draw border
258
+ for (let y = y0; y < y1 && y < height; y++) {
259
+ for (let x = x0; x < x1 && x < width; x++) {
260
+ const isTop = y === y0;
261
+ const isBottom = y === y1 - 1;
262
+ const isLeft = x === x0;
263
+ const isRight = x === x1 - 1;
264
+
265
+ // Only draw borders
266
+ if (isTop || isBottom || isLeft || isRight) {
267
+ let char = ' ';
268
+ if (isTop && isLeft) char = '┌';else if (isTop && isRight) char = '┐';else if (isBottom && isLeft) char = '└';else if (isBottom && isRight) char = '┘';else if (isTop || isBottom) char = '─';else if (isLeft || isRight) char = '│';
269
+ grid[y][x] = {
270
+ char,
271
+ color
272
+ };
273
+ }
274
+ }
275
+ }
276
+
277
+ // Draw label if there's room
278
+ const name = node.data.name;
279
+ const sizeStr = node.value ? ` ${formatBytes(node.value)}` : '';
280
+ if (boxWidth >= 8 && boxHeight >= 2) {
281
+ const labelY = y0 + 1;
282
+ const labelX = x0 + 1;
283
+ const maxLength = boxWidth - 2;
284
+
285
+ // Combine name and size
286
+ let label = name + sizeStr;
287
+ if (label.length > maxLength) {
288
+ // Try just name
289
+ if (name.length <= maxLength) {
290
+ label = name;
291
+ } else {
292
+ label = name.slice(0, maxLength - 1) + '…';
293
+ }
294
+ }
295
+ if (labelY >= 0 && labelY < height && labelY < y1) {
296
+ for (let i = 0; i < label.length && labelX + i < x1 - 1 && labelX + i < width; i++) {
297
+ grid[labelY][labelX + i] = {
298
+ char: label[i],
299
+ color
300
+ };
301
+ }
302
+ }
303
+ } else if (boxWidth >= 4 && boxHeight >= 2) {
304
+ // Show abbreviated label
305
+ const labelY = y0 + 1;
306
+ const labelX = x0 + 1;
307
+ const maxLength = boxWidth - 2;
308
+ let label;
309
+ if (isFile) {
310
+ // Show extension for files
311
+ const ext = name.match(/\.([^.]{1,3})$/)?.[1];
312
+ label = ext || name.slice(0, maxLength);
313
+ } else {
314
+ // Show first few chars for directories
315
+ label = name.slice(0, maxLength);
316
+ }
317
+ if (labelY >= 0 && labelY < height && label) {
318
+ for (let i = 0; i < label.length && labelX + i < x1 - 1 && labelX + i < width; i++) {
319
+ grid[labelY][labelX + i] = {
320
+ char: label[i],
321
+ color
322
+ };
323
+ }
324
+ }
325
+ } else if (boxWidth >= 2 && boxHeight >= 2) {
326
+ // Just first character
327
+ const centerY = Math.floor((y0 + y1) / 2);
328
+ const centerX = Math.floor((x0 + x1) / 2);
329
+ if (centerY >= 0 && centerY < height && centerX >= 0 && centerX < width) {
330
+ grid[centerY][centerX] = {
331
+ char: name[0] || '•',
332
+ color
333
+ };
334
+ }
335
+ } else if (boxWidth === 1 && boxHeight === 1) {
336
+ // Tiny dot
337
+ if (y0 >= 0 && y0 < height && x0 >= 0 && x0 < width) {
338
+ grid[y0][x0] = {
339
+ char: '·',
340
+ color
341
+ };
342
+ }
343
+ }
344
+ });
345
+
346
+ // Convert grid to Ink components
347
+ const lines = grid.map((row, y) => {
348
+ const segments = [];
349
+ let currentColor = null;
350
+ let currentText = '';
351
+ row.forEach(cell => {
352
+ if (cell.color === currentColor) {
353
+ currentText += cell.char;
354
+ } else {
355
+ if (currentText) {
356
+ segments.push(/*#__PURE__*/React.createElement(Text, {
357
+ key: segments.length,
358
+ color: currentColor
359
+ }, currentText));
360
+ }
361
+ currentColor = cell.color;
362
+ currentText = cell.char;
363
+ }
364
+ });
365
+ if (currentText) {
366
+ segments.push(/*#__PURE__*/React.createElement(Text, {
367
+ key: segments.length,
368
+ color: currentColor
369
+ }, currentText));
370
+ }
371
+ return /*#__PURE__*/React.createElement(Box, {
372
+ key: y
373
+ }, segments);
374
+ });
375
+ return /*#__PURE__*/React.createElement(Box, {
376
+ flexDirection: "column"
377
+ }, /*#__PURE__*/React.createElement(Text, {
378
+ bold: true,
379
+ color: oneHunter.cyan
380
+ }, "TreeMap View: ", selectedNode.data.name), /*#__PURE__*/React.createElement(Text, {
381
+ dimColor: true,
382
+ color: oneHunter.textAlt
383
+ }, "Press 't' to toggle back to tree view | \u2191/\u2193: Navigate in tree | Ctrl+C: Exit"), /*#__PURE__*/React.createElement(Box, {
384
+ borderStyle: "single",
385
+ borderColor: oneHunter.textAlt,
386
+ flexDirection: "column",
387
+ marginTop: 1
388
+ }, lines), /*#__PURE__*/React.createElement(Text, {
389
+ dimColor: true,
390
+ color: oneHunter.textAlt,
391
+ marginTop: 1
392
+ }, "Showing ", firstLevelNodes.length, " items | Total: ", formatBytes(root.value || 0)));
393
+ };
package/dist/app.js ADDED
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ export default function App({
4
+ name = 'Stranger'
5
+ }) {
6
+ return /*#__PURE__*/React.createElement(Text, null, "Hello, ", /*#__PURE__*/React.createElement(Text, {
7
+ color: "green"
8
+ }, name));
9
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ import React, { useState, useEffect, useMemo } from 'react';
3
+ import { render, Text, Box, useInput, useStdout } from 'ink';
4
+ import { hierarchy } from 'd3-hierarchy';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { TreeMapView } from './Treemap.js';
8
+ const oneHunter = {
9
+ bg: '#282c34',
10
+ bgAlt: '#21252b',
11
+ text: '#abb2bf',
12
+ textAlt: 'white',
13
+ red: '#e06c75',
14
+ orange: 'rgb(218, 112, 44)',
15
+ yellow: '#e5c07b',
16
+ green: '#98c379',
17
+ cyan: 'rgb(58, 169, 159)',
18
+ blue: 'rgb(67, 133, 190)',
19
+ purple: 'rgb(139, 126, 200)',
20
+ magenta: 'rgb(206, 93, 151)'
21
+ };
22
+
23
+ // Function to format bytes to human readable
24
+ const formatBytes = bytes => {
25
+ if (bytes === 0) return '0 B';
26
+ const k = 1024;
27
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
28
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
29
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
30
+ };
31
+
32
+ // Function to get directory size recursively
33
+ const getDirSize = dirPath => {
34
+ let size = 0;
35
+ try {
36
+ const items = fs.readdirSync(dirPath);
37
+ for (const item of items) {
38
+ const itemPath = path.join(dirPath, item);
39
+ try {
40
+ const stats = fs.statSync(itemPath);
41
+ if (stats.isFile()) {
42
+ size += stats.size;
43
+ } else if (stats.isDirectory()) {
44
+ size += getDirSize(itemPath);
45
+ }
46
+ } catch {}
47
+ }
48
+ } catch {}
49
+ return size;
50
+ };
51
+
52
+ // Function to read directory structure (unbounded depth)
53
+ const readDirTree = dirPath => {
54
+ try {
55
+ const stats = fs.statSync(dirPath);
56
+ const name = path.basename(dirPath) || dirPath;
57
+ if (!stats.isDirectory()) {
58
+ const size = stats.size;
59
+ return {
60
+ name,
61
+ path: dirPath,
62
+ isFile: true,
63
+ size,
64
+ value: size > 0 ? size : 100
65
+ };
66
+ }
67
+ const children = fs.readdirSync(dirPath).filter(file => !file.startsWith('.')).map(file => readDirTree(path.join(dirPath, file))).filter(Boolean);
68
+ return {
69
+ name,
70
+ path: dirPath,
71
+ isFile: false,
72
+ size: getDirSize(dirPath),
73
+ children: children.length > 0 ? children : undefined
74
+ };
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ // Get color based on file extension
81
+ const getFileColor = filename => {
82
+ const ext = path.extname(filename).toLowerCase();
83
+ const colorMap = {
84
+ '.js': oneHunter.yellow,
85
+ '.jsx': oneHunter.yellow,
86
+ '.ts': oneHunter.blue,
87
+ '.tsx': oneHunter.blue,
88
+ '.py': oneHunter.green,
89
+ '.rb': oneHunter.red,
90
+ '.json': oneHunter.purple,
91
+ '.html': oneHunter.cyan,
92
+ '.css': oneHunter.cyan,
93
+ '.md': oneHunter.text,
94
+ '.txt': oneHunter.textAlt,
95
+ '.sh': oneHunter.green,
96
+ '.yml': oneHunter.purple,
97
+ '.yaml': oneHunter.purple,
98
+ '.xml': oneHunter.orange,
99
+ '.svg': oneHunter.cyan,
100
+ '.png': oneHunter.blue,
101
+ '.jpg': oneHunter.blue,
102
+ '.gif': oneHunter.blue,
103
+ '.pdf': oneHunter.red
104
+ };
105
+ return colorMap[ext] || oneHunter.text;
106
+ };
107
+
108
+ // Generate shortcut label
109
+ const generateShortcut = (index, isFile, parentShortcut = '') => {
110
+ const letters = 'abcdefghijklmnoprsuvwxyz'; // omitting q, t
111
+ const base = letters.length;
112
+ if (isFile) return `${parentShortcut}${index + 1}`;
113
+ let label = '';
114
+ let num = index;
115
+ while (num >= 0) {
116
+ label = letters[num % base] + label;
117
+ num = Math.floor(num / base) - 1;
118
+ }
119
+ return parentShortcut + label;
120
+ };
121
+
122
+ // TreeRow
123
+ const TreeRow = /*#__PURE__*/React.memo(({
124
+ node,
125
+ isSelected,
126
+ collapsed,
127
+ shortcutInput
128
+ }) => {
129
+ const isCollapsed = collapsed.has(node.data.path);
130
+ const hasChildren = node.children && node.children.length > 0;
131
+ let prefix = '';
132
+ let current = node;
133
+ const segments = [];
134
+ while (current.parent) {
135
+ const parent = current.parent;
136
+ const siblings = parent.children || [];
137
+ const isLastChild = siblings[siblings.length - 1] === current;
138
+ segments.unshift(isLastChild ? ' ' : '│ ');
139
+ current = parent;
140
+ }
141
+ if (node.parent) {
142
+ const siblings = node.parent.children || [];
143
+ const isLastChild = siblings[siblings.length - 1] === node;
144
+ prefix = segments.join('').slice(0, -2) + (isLastChild ? '└─' : '├─');
145
+ }
146
+ let icon = hasChildren ? isCollapsed ? '▶ ' : '▼ ' : ' ';
147
+ const fileColor = node.data.isFile ? getFileColor(node.data.name) : oneHunter.magenta;
148
+ const renderShortcut = () => {
149
+ if (!shortcutInput) return /*#__PURE__*/React.createElement(Text, {
150
+ color: oneHunter.textAlt,
151
+ dimColor: true
152
+ }, ' ', "[", node.shortcut, "]");
153
+ if (node.shortcut.startsWith(shortcutInput)) {
154
+ const matched = node.shortcut.slice(0, shortcutInput.length);
155
+ const rest = node.shortcut.slice(shortcutInput.length);
156
+ return /*#__PURE__*/React.createElement(Text, {
157
+ color: oneHunter.textAlt
158
+ }, ' [', /*#__PURE__*/React.createElement(Text, {
159
+ underline: true,
160
+ color: oneHunter.yellow
161
+ }, matched), rest, ']');
162
+ }
163
+ return /*#__PURE__*/React.createElement(Text, {
164
+ color: oneHunter.textAlt,
165
+ dimColor: true
166
+ }, ' ', "[", node.shortcut, "]");
167
+ };
168
+ return /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
169
+ color: oneHunter.textAlt
170
+ }, prefix), /*#__PURE__*/React.createElement(Text, {
171
+ color: hasChildren ? oneHunter.cyan : oneHunter.textAlt
172
+ }, icon), /*#__PURE__*/React.createElement(Text, {
173
+ bold: isSelected,
174
+ color: isSelected ? oneHunter.bg : fileColor,
175
+ backgroundColor: isSelected ? oneHunter.green : undefined
176
+ }, node.data.name), hasChildren && !isCollapsed && /*#__PURE__*/React.createElement(Text, {
177
+ color: oneHunter.textAlt
178
+ }, " (", node.children.length, ")"), node.data.size > 0 && /*#__PURE__*/React.createElement(Text, {
179
+ color: oneHunter.green
180
+ }, " ", formatBytes(node.data.size)), renderShortcut()));
181
+ }, (p, n) => p.isSelected === n.isSelected && p.collapsed === n.collapsed && p.shortcutInput === n.shortcutInput);
182
+ const TreeVisualization = ({
183
+ dirPath = '.',
184
+ maxLevel = 1
185
+ }) => {
186
+ const [selectedIndex, setSelectedIndex] = useState(0);
187
+ const [collapsed, setCollapsed] = useState(new Set());
188
+ const [shortcutInput, setShortcutInput] = useState('');
189
+ const [showTreeMap, setShowTreeMap] = useState(false);
190
+ const [initialized, setInitialized] = useState(false);
191
+ const {
192
+ stdout
193
+ } = useStdout();
194
+ const terminalHeight = stdout?.rows || 24;
195
+ const viewportHeight = Math.max(5, terminalHeight - 7);
196
+ const treeData = useMemo(() => readDirTree(dirPath), [dirPath]);
197
+ if (!treeData) return /*#__PURE__*/React.createElement(Text, {
198
+ color: "red"
199
+ }, "Error: Could not read directory");
200
+ const root = useMemo(() => hierarchy(treeData), [treeData]);
201
+
202
+ // --- Initial collapse based on -L ---
203
+ useEffect(() => {
204
+ const newCollapsed = new Set();
205
+ const applyLevel = (node, level = 0) => {
206
+ if (node.children) {
207
+ if (level >= maxLevel) newCollapsed.add(node.data.path);
208
+ node.children.forEach(child => applyLevel(child, level + 1));
209
+ }
210
+ };
211
+ applyLevel(root);
212
+ setCollapsed(newCollapsed);
213
+ setInitialized(true);
214
+ }, [maxLevel, root]);
215
+
216
+ // Assign shortcuts
217
+ const rootWithShortcuts = useMemo(() => {
218
+ const assignShortcuts = (node, parentShortcut = '', siblingIndex = 0) => {
219
+ const isFile = node.data.isFile;
220
+ node.shortcut = generateShortcut(siblingIndex, isFile, parentShortcut);
221
+ if (node.children) {
222
+ node.children.forEach((child, i) => assignShortcuts(child, node.shortcut, i));
223
+ }
224
+ };
225
+ const cloned = root.copy();
226
+ assignShortcuts(cloned);
227
+ return cloned;
228
+ }, [root]);
229
+
230
+ // Flatten visible nodes
231
+ const visibleNodes = useMemo(() => {
232
+ const nodes = [];
233
+ const traverse = node => {
234
+ nodes.push(node);
235
+ if (node.children && !collapsed.has(node.data.path)) {
236
+ node.children.forEach(traverse);
237
+ }
238
+ };
239
+ traverse(rootWithShortcuts);
240
+ return nodes;
241
+ }, [rootWithShortcuts, collapsed]);
242
+ const shortcutMap = useMemo(() => {
243
+ const map = new Map();
244
+ visibleNodes.forEach((node, index) => map.set(node.shortcut, index));
245
+ return map;
246
+ }, [visibleNodes]);
247
+ useInput((input, key) => {
248
+ if (input === 'q') process.exit(0);
249
+ if (input === 't') {
250
+ setShowTreeMap(p => !p);
251
+ return;
252
+ }
253
+ if (showTreeMap) return;
254
+ if (key.upArrow) {
255
+ setSelectedIndex(p => Math.max(0, p - 1));
256
+ setShortcutInput('');
257
+ return;
258
+ } else if (key.downArrow) {
259
+ setSelectedIndex(p => Math.min(visibleNodes.length - 1, p + 1));
260
+ setShortcutInput('');
261
+ return;
262
+ }
263
+
264
+ // ✅ ENTER behavior
265
+ if (key.return) {
266
+ if (shortcutInput && shortcutMap.has(shortcutInput)) {
267
+ const shortcutIndex = shortcutMap.get(shortcutInput);
268
+ const shortcutNode = visibleNodes[shortcutIndex];
269
+
270
+ // Jump and toggle collapse (in one go)
271
+ setSelectedIndex(shortcutIndex);
272
+ if (shortcutNode.children) {
273
+ setCollapsed(prev => {
274
+ const next = new Set(prev);
275
+ if (next.has(shortcutNode.data.path)) next.delete(shortcutNode.data.path);else next.add(shortcutNode.data.path);
276
+ return next;
277
+ });
278
+ }
279
+ setShortcutInput('');
280
+ return;
281
+ }
282
+
283
+ // Otherwise toggle current node
284
+ const node = visibleNodes[selectedIndex];
285
+ if (node.children) {
286
+ setCollapsed(prev => {
287
+ const next = new Set(prev);
288
+ if (next.has(node.data.path)) next.delete(node.data.path);else next.add(node.data.path);
289
+ return next;
290
+ });
291
+ }
292
+ setShortcutInput('');
293
+ return;
294
+ }
295
+ if (key.escape) {
296
+ setShortcutInput('');
297
+ return;
298
+ }
299
+
300
+ // Typing a shortcut
301
+ if (input && /^[a-z0-9]$/.test(input)) {
302
+ const newInput = shortcutInput + input;
303
+ setShortcutInput(newInput);
304
+
305
+ // Move highlight immediately
306
+ if (shortcutMap.has(newInput)) {
307
+ setSelectedIndex(shortcutMap.get(newInput));
308
+ }
309
+ }
310
+ });
311
+ if (showTreeMap) {
312
+ const node = visibleNodes[selectedIndex];
313
+ if (!node) return /*#__PURE__*/React.createElement(Text, {
314
+ color: oneHunter.red
315
+ }, "Error: No node selected");
316
+ return /*#__PURE__*/React.createElement(TreeMapView, {
317
+ selectedNode: node,
318
+ stdout: stdout
319
+ });
320
+ }
321
+
322
+ // --- Viewport logic ---
323
+ const getViewportWindow = () => {
324
+ let start = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
325
+ let end = start + viewportHeight;
326
+ if (end > visibleNodes.length) {
327
+ end = visibleNodes.length;
328
+ start = Math.max(0, end - viewportHeight);
329
+ }
330
+ return {
331
+ start,
332
+ end
333
+ };
334
+ };
335
+ const {
336
+ start,
337
+ end
338
+ } = getViewportWindow();
339
+ const viewportNodes = visibleNodes.slice(start, end);
340
+ if (!initialized) return null;
341
+ return /*#__PURE__*/React.createElement(Box, {
342
+ flexDirection: "column"
343
+ }, /*#__PURE__*/React.createElement(Text, {
344
+ color: oneHunter.textAlt,
345
+ dimColor: true
346
+ }, "\u2191/\u2193: Navigate | Enter: Toggle | t: TreeMap | Esc: Clear | q: Quit"), start > 0 && /*#__PURE__*/React.createElement(Text, {
347
+ color: oneHunter.yellow
348
+ }, "\u22EE (", start, " more above)"), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
349
+ key: start + i,
350
+ node: node,
351
+ isSelected: start + i === selectedIndex,
352
+ collapsed: collapsed,
353
+ shortcutInput: shortcutInput
354
+ })), end < visibleNodes.length && /*#__PURE__*/React.createElement(Text, {
355
+ color: oneHunter.yellow
356
+ }, "\u22EE (", visibleNodes.length - end, " more below)"));
357
+ };
358
+
359
+ // --- CLI args ---
360
+ const args = process.argv.slice(2);
361
+
362
+ // Dir flag
363
+ const dirIndex = args.indexOf('--dir');
364
+ let dirPath;
365
+ const firstNonFlagArg = args.find(arg => !arg.startsWith('-'));
366
+ if (dirIndex !== -1 && args[dirIndex + 1]) {
367
+ dirPath = args[dirIndex + 1];
368
+ } else if (firstNonFlagArg) {
369
+ dirPath = firstNonFlagArg;
370
+ } else {
371
+ dirPath = '.';
372
+ }
373
+
374
+ // Handle -L flag
375
+ const levelIndex = args.indexOf('-L');
376
+ const maxLevel = levelIndex !== -1 && args[levelIndex + 1] ? parseInt(args[levelIndex + 1], 10) : 1;
377
+ render(/*#__PURE__*/React.createElement(TreeVisualization, {
378
+ dirPath: dirPath,
379
+ maxLevel: maxLevel
380
+ }));
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "tri-cli",
3
+ "version": "0.0.2",
4
+ "description": "Interactive CLI directory tree visualizer with treemap view",
5
+ "author": "Jared Wilber",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "cli",
9
+ "tree",
10
+ "directory",
11
+ "treemap",
12
+ "visualization",
13
+ "interactive"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/jwilber/tri-cli.git"
18
+ },
19
+ "bin": {
20
+ "tri": "dist/cli.js"
21
+ },
22
+ "type": "module",
23
+ "engines": {
24
+ "node": ">=16"
25
+ },
26
+ "scripts": {
27
+ "build": "babel --out-dir=dist source",
28
+ "postbuild": "chmod +x dist/cli.js",
29
+ "dev": "babel --out-dir=dist --watch source",
30
+ "test": "prettier --check . && xo && ava",
31
+ "test:local": "npm run build && tri --dir . -L 2"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "dependencies": {
37
+ "d3-hierarchy": "^3.1.2",
38
+ "ink": "^4.1.0",
39
+ "meow": "^11.0.0",
40
+ "react": "^18.2.0"
41
+ },
42
+ "devDependencies": {
43
+ "@babel/cli": "^7.21.0",
44
+ "@babel/preset-react": "^7.28.5",
45
+ "@vdemedes/prettier-config": "^2.0.1",
46
+ "ava": "^5.2.0",
47
+ "chalk": "^5.2.0",
48
+ "eslint-config-xo-react": "^0.27.0",
49
+ "eslint-plugin-react": "^7.32.2",
50
+ "eslint-plugin-react-hooks": "^4.6.0",
51
+ "import-jsx": "^5.0.0",
52
+ "ink-testing-library": "^3.0.0",
53
+ "prettier": "^2.8.7",
54
+ "xo": "^0.53.1"
55
+ },
56
+ "ava": {
57
+ "environmentVariables": {
58
+ "NODE_NO_WARNINGS": "1"
59
+ },
60
+ "nodeArguments": [
61
+ "--loader=import-jsx"
62
+ ]
63
+ },
64
+ "xo": {
65
+ "extends": "xo-react",
66
+ "prettier": true,
67
+ "rules": {
68
+ "react/prop-types": "off"
69
+ }
70
+ },
71
+ "prettier": "@vdemedes/prettier-config",
72
+ "babel": {
73
+ "presets": [
74
+ "@babel/preset-react"
75
+ ]
76
+ }
77
+ }
package/readme.md ADDED
@@ -0,0 +1,25 @@
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
+ ```