tri-cli 0.0.53 → 0.0.55

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.
Files changed (2) hide show
  1. package/dist/cli.js +46 -80
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -73,33 +73,19 @@ const getDirSizeAsync = async dirPath => {
73
73
  }
74
74
  };
75
75
 
76
- // Fast file counting - much faster than size calculation
76
+ // Fast file counting using shell command
77
77
  const countFilesAsync = async dirPath => {
78
78
  try {
79
79
  const stats = fs.statSync(dirPath);
80
80
  if (!stats.isDirectory()) return 1;
81
- let count = 0;
82
- const countRecursive = async dir => {
83
- try {
84
- const entries = fs.readdirSync(dir, {
85
- withFileTypes: true
86
- });
87
- for (const entry of entries) {
88
- // Skip hidden files
89
- if (entry.name.startsWith('.')) continue;
90
- const fullPath = path.join(dir, entry.name);
91
- if (entry.isFile()) {
92
- count++;
93
- } else if (entry.isDirectory()) {
94
- await countRecursive(fullPath);
95
- }
96
- }
97
- } catch {
98
- // Skip directories we can't read
99
- }
100
- };
101
- await countRecursive(dirPath);
102
- return count;
81
+
82
+ // Use find command for fast counting, excluding hidden files
83
+ const escapedPath = dirPath.replace(/"/g, '\\"');
84
+ const {
85
+ stdout
86
+ } = await execAsync(`find "${escapedPath}" -type f ! -path '*/.*' 2>/dev/null | wc -l`);
87
+ const count = parseInt(stdout.trim(), 10);
88
+ return isNaN(count) ? 0 : count;
103
89
  } catch {
104
90
  return 0;
105
91
  }
@@ -279,13 +265,18 @@ const TreeRow = /*#__PURE__*/React.memo(({
279
265
  color: oneHunter.purple
280
266
  }, " [", formatCount(fileCount), " files]"), isLoading && /*#__PURE__*/React.createElement(Text, {
281
267
  color: oneHunter.yellow
282
- }, " ", SPINNER_FRAMES[spinnerFrame])), isLoading ? /*#__PURE__*/React.createElement(Text, {
268
+ }, " ", SPINNER_FRAMES[spinnerFrame])), isLoading && fileCount === undefined ? /*#__PURE__*/React.createElement(Text, {
283
269
  color: oneHunter.yellow
284
270
  }, " ", SPINNER_FRAMES[spinnerFrame]) : displaySize > 0 ? /*#__PURE__*/React.createElement(Text, {
285
271
  color: oneHunter.cyan
286
272
  }, " ", formatBytes(displaySize)) : /*#__PURE__*/React.createElement(Text, null, " "), renderShortcut(), lastModified && /*#__PURE__*/React.createElement(Text, {
287
273
  color: oneHunter.text2
288
274
  }, " ", formatDate(lastModified))));
275
+ },
276
+ // Custom comparison function to prevent unnecessary re-renders
277
+ (prevProps, nextProps) => {
278
+ // Only re-render if these specific props change
279
+ return prevProps.node.data.path === nextProps.node.data.path && prevProps.isSelected === nextProps.isSelected && prevProps.collapsed.has(prevProps.node.data.path) === nextProps.collapsed.has(nextProps.node.data.path) && prevProps.shortcutInput === nextProps.shortcutInput && prevProps.calculatedSizes.get(prevProps.node.data.path) === nextProps.calculatedSizes.get(nextProps.node.data.path) && prevProps.calculatedCounts.get(prevProps.node.data.path) === nextProps.calculatedCounts.get(nextProps.node.data.path) && prevProps.loadingPaths.has(prevProps.node.data.path) === nextProps.loadingPaths.has(nextProps.node.data.path) && prevProps.lastModifiedDates.get(prevProps.node.data.path) === nextProps.lastModifiedDates.get(nextProps.node.data.path) && prevProps.spinnerFrame === nextProps.spinnerFrame;
289
280
  });
290
281
  const TreeVisualization = ({
291
282
  dirPath = '.',
@@ -451,64 +442,39 @@ const TreeVisualization = ({
451
442
  collectNodes(node);
452
443
  if (nodesToCount.length === 0) return; // Nothing to count
453
444
 
445
+ // Sort nodes by depth (shallowest first) for top-down display
446
+ nodesToCount.sort((a, b) => a.depth - b.depth);
447
+
454
448
  // Mark all nodes as loading
455
- const pathsToLoad = nodesToCount.map(n => n.data.path);
456
- setLoadingPaths(prev => new Set([...prev, ...pathsToLoad]));
457
- const newMap = new Map(calculatedCounts);
458
-
459
- // Count and stream updates for each node individually
460
- for (const targetNode of nodesToCount) {
461
- // For directories, stream partial counts as we count
462
- if (!targetNode.data.isFile) {
463
- let runningCount = 0;
464
- let updateCounter = 0;
465
- const countRecursive = async dir => {
466
- try {
467
- const entries = fs.readdirSync(dir, {
468
- withFileTypes: true
469
- });
470
- for (const entry of entries) {
471
- // Skip hidden files
472
- if (entry.name.startsWith('.')) continue;
473
- const fullPath = path.join(dir, entry.name);
474
- if (entry.isFile()) {
475
- runningCount++;
476
- updateCounter++;
477
- // Stream update every 100 files for better performance
478
- if (updateCounter >= 100) {
479
- updateCounter = 0;
480
- newMap.set(targetNode.data.path, runningCount);
481
- React.startTransition(() => {
482
- setCalculatedCounts(new Map(newMap));
483
- });
484
- await new Promise(r => setTimeout(r, 0));
485
- }
486
- } else if (entry.isDirectory()) {
487
- await countRecursive(fullPath);
488
- }
489
- }
490
- } catch {
491
- // Skip directories we can't read
492
- }
493
- };
494
- await countRecursive(targetNode.data.path);
495
- newMap.set(targetNode.data.path, runningCount);
496
- } else {
497
- // Files are always 1
498
- newMap.set(targetNode.data.path, 1);
499
- }
449
+ setLoadingPaths(new Set(nodesToCount.map(n => n.data.path)));
500
450
 
501
- // Stream the final update and remove from loading
502
- React.startTransition(() => {
503
- setCalculatedCounts(new Map(newMap));
504
- setLoadingPaths(prev => {
505
- const next = new Set(prev);
506
- next.delete(targetNode.data.path);
507
- return next;
508
- });
451
+ // Track completed counts
452
+ const completedCounts = new Map();
453
+ const completedPaths = new Set();
454
+
455
+ // Process all nodes in parallel
456
+ const promises = nodesToCount.map(async targetNode => {
457
+ const count = await countFilesAsync(targetNode.data.path);
458
+
459
+ // Store result
460
+ completedCounts.set(targetNode.data.path, count);
461
+ completedPaths.add(targetNode.data.path);
462
+
463
+ // Batch update state (React will batch these automatically)
464
+ setCalculatedCounts(prev => {
465
+ const next = new Map(prev);
466
+ next.set(targetNode.data.path, count);
467
+ return next;
509
468
  });
510
- await new Promise(r => setTimeout(r, 0));
511
- }
469
+ setLoadingPaths(prev => {
470
+ const next = new Set(prev);
471
+ next.delete(targetNode.data.path);
472
+ return next;
473
+ });
474
+ });
475
+
476
+ // Wait for all counts to complete
477
+ await Promise.all(promises);
512
478
  };
513
479
  const getLastModifiedAsync = async node => {
514
480
  // Collect node and all its descendants
@@ -834,7 +800,7 @@ const TreeVisualization = ({
834
800
  }, "\u2191/\u2193: Navigate | Enter: Expand/Traverse | c: Count | d: Size | l: Date | m: Map | Shift+C: CountMap | q: Quit"), loadingPaths.size > 0 && /*#__PURE__*/React.createElement(Text, {
835
801
  color: oneHunter.yellow
836
802
  }, ' ', SPINNER_FRAMES[spinnerFrame], " Loading ", loadingPaths.size, "...")), viewportNodes.map((node, i) => /*#__PURE__*/React.createElement(TreeRow, {
837
- key: start + i,
803
+ key: node.data.path,
838
804
  node: node,
839
805
  isSelected: start + i === selectedIndex,
840
806
  collapsed: collapsed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tri-cli",
3
- "version": "0.0.53",
3
+ "version": "0.0.55",
4
4
  "description": "Interactive CLI directory tree visualizer with treemap view",
5
5
  "author": "Jared Wilber",
6
6
  "license": "MIT",