@codewithmehmet/paul-cli
Version:
Intelligent project file scanner and Git change tracker with interactive interface
197 lines (191 loc) β’ 7.19 kB
JavaScript
import React, { useState, useCallback } from 'react';
import { Box, Text } from 'ink';
import { useInput } from 'ink';
import { formatSize } from '../utils/output.js';
export default function FileSelector({
items,
selectedFiles,
onSelectionChange,
onBack,
onExport,
onOpenPresets
}) {
const [expandedDirs, setExpandedDirs] = useState(new Set());
const [pointer, setPointer] = useState(0);
const [flatItems, setFlatItems] = useState([]);
// Flatten items based on expanded directories
React.useEffect(() => {
const flatten = (items, level = 0, parentPath = '') => {
const result = [];
for (const item of items) {
const itemPath = item.path;
result.push({
...item,
level,
parentPath
});
if (item.children && expandedDirs.has(itemPath)) {
result.push(...flatten(item.children, level + 1, itemPath));
}
}
return result;
};
setFlatItems(flatten(items));
}, [items, expandedDirs]);
// Calculate stats
const stats = React.useMemo(() => {
let totalSize = 0;
let fileCount = 0;
let lineCount = 0;
for (const path of selectedFiles) {
// Find item by path
const findItem = items => {
for (const item of items) {
if (item.path === path) {
if (item.type === 'file') {
totalSize += item.size || 0;
fileCount++;
// Utiliser les vraies lignes si disponibles
if (item.lines) {
lineCount += item.lines;
} else {
// Sinon utiliser la mΓͺme estimation que dans l'affichage
lineCount += Math.max(1, Math.round((item.size || 0) / 40));
}
}
}
if (item.children) {
findItem(item.children);
}
}
};
findItem(items);
}
return {
size: totalSize,
files: fileCount,
lines: lineCount
};
}, [selectedFiles, items]);
const toggleExpand = useCallback(item => {
if (item.type === 'directory') {
const newExpanded = new Set(expandedDirs);
if (newExpanded.has(item.path)) {
newExpanded.delete(item.path);
} else {
newExpanded.add(item.path);
}
setExpandedDirs(newExpanded);
}
}, [expandedDirs]);
const toggleSelection = useCallback(item => {
const newSelection = new Set(selectedFiles);
const toggleItemAndChildren = item => {
if (newSelection.has(item.path)) {
newSelection.delete(item.path);
} else {
newSelection.add(item.path);
}
// Toggle children if directory
if (item.children) {
for (const child of item.children) {
toggleItemAndChildren(child);
}
}
};
toggleItemAndChildren(item);
onSelectionChange(newSelection);
}, [selectedFiles, onSelectionChange]);
const selectAll = useCallback(() => {
const allPaths = new Set();
const collectPaths = items => {
for (const item of items) {
// Ajouter tous les items, pas seulement les fichiers
allPaths.add(item.path);
if (item.children) {
collectPaths(item.children);
}
}
};
collectPaths(items);
onSelectionChange(allPaths);
}, [items, onSelectionChange]);
const clearSelection = useCallback(() => {
onSelectionChange(new Set());
}, [onSelectionChange]);
useInput((input, key) => {
if (key.escape || input === 'q') {
onBack();
} else if (key.upArrow) {
setPointer(Math.max(0, pointer - 1));
} else if (key.downArrow) {
setPointer(Math.min(flatItems.length - 1, pointer + 1));
} else if (key.return || key.rightArrow) {
const item = flatItems[pointer];
if (item) {
toggleExpand(item);
}
} else if (key.leftArrow) {
const item = flatItems[pointer];
if (item && expandedDirs.has(item.path)) {
toggleExpand(item);
}
} else if (input === ' ') {
const item = flatItems[pointer];
if (item) {
toggleSelection(item);
}
} else if (input === 'a') {
selectAll();
} else if (input === 'c') {
clearSelection();
} else if (input === 'o' && selectedFiles.size > 0) {
onExport();
} else if (input === 'p') {
onOpenPresets?.();
}
});
// Calculate visible range (window of 15 items)
const windowSize = 15;
const scrollOffset = Math.max(0, Math.min(pointer - Math.floor(windowSize / 2), flatItems.length - windowSize));
const visibleItems = flatItems.slice(scrollOffset, scrollOffset + windowSize);
// Dans le return, ajouter des espaces entre les sections :
return /*#__PURE__*/React.createElement(Box, {
flexDirection: "column"
}, /*#__PURE__*/React.createElement(Box, {
flexDirection: "row"
}, /*#__PURE__*/React.createElement(Text, {
color: "green",
bold: true
}, "\uD83D\uDCC2 File Scanner"), /*#__PURE__*/React.createElement(Box, {
marginLeft: 2
}, /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, stats.files, " files | ", formatSize(stats.size), " | ", stats.lines, " lines"))), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, 'β'.repeat(50)), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Box, {
flexDirection: "column"
}, visibleItems.map((item, index) => {
const actualIndex = scrollOffset + index;
const isPointed = actualIndex === pointer;
const isSelected = selectedFiles.has(item.path);
const isExpanded = expandedDirs.has(item.path);
// Calculer les lignes
const lines = item.lines || item.type === 'file' && item.size && Math.max(1, Math.round(item.size / 40)) || 0;
return /*#__PURE__*/React.createElement(Box, {
key: item.path
}, /*#__PURE__*/React.createElement(Text, {
color: isPointed ? 'cyan' : 'white'
}, isPointed ? 'βΆ ' : ' ', ' '.repeat(item.level), item.type === 'directory' ? isExpanded ? 'π' : 'π' : 'π', ' ', isSelected ? '[β] ' : '[ ] ', item.name, item.size && /*#__PURE__*/React.createElement(React.Fragment, null, ' ', /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, "(", formatSize(item.size), ")", lines > 0 && ` - ${lines} line${lines !== 1 ? 's' : ''}`))));
})), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, 'β'.repeat(50)), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, "\u2191\u2193 Navigate | \u23CE/\u2192 Expand | \u2190 Collapse | \u2423 Select"), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, "a Select all | c Clear | t Toggle tree | p Presets |", ' ', selectedFiles.size > 0 ? 'o Output | ' : '', "ESC Back"), flatItems.length > windowSize && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, scrollOffset + 1, "-", Math.min(scrollOffset + windowSize, flatItems.length), " of", ' ', flatItems.length)));
}