@codewithmehmet/paul-cli
Version:
Intelligent project file scanner and Git change tracker with interactive interface
177 lines (174 loc) • 6.58 kB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text } from 'ink';
import { useInput } from 'ink';
import TextInput from 'ink-text-input';
import { Scanner } from '../core/scanner.js';
import { generateOutput, generateOutputFilename } from '../utils/output.js';
import { copyToClipboard } from '../utils/clipboard.js';
import FileSelector from './FileSelector.js';
import PresetManager from './PresetManager.js';
import fs from 'fs/promises';
export default function LsInteractive({
onBack
}) {
const [items, setItems] = useState([]);
const [selectedFiles, setSelectedFiles] = useState(new Set());
const [isLoading, setIsLoading] = useState(true);
const [mode, setMode] = useState('select'); // 'select', 'output', 'preset'
const [message, setMessage] = useState('');
const [exportOption, setExportOption] = useState(0); // 0: clipboard, 1: file, 2: toggle tree
const [includeFullTree, setIncludeFullTree] = useState(true);
const [fileNameInput, setFileNameInput] = useState('');
const [isTypingFileName, setIsTypingFileName] = useState(false);
// Load files on mount
useEffect(() => {
loadFiles();
}, []);
const loadFiles = async () => {
setIsLoading(true);
const scanner = new Scanner({
path: process.cwd(),
includeContent: false
});
const scannedItems = await scanner.scan();
setItems(scannedItems);
setIsLoading(false);
};
const handleSelectionChange = useCallback(newSelection => {
setSelectedFiles(newSelection);
}, []);
const handleApplyPreset = useCallback(newSelection => {
setSelectedFiles(newSelection);
setMode('select');
}, []);
const handleExport = async type => {
if (type === 'file' && !isTypingFileName) {
// Demander le nom du fichier
setIsTypingFileName(true);
setFileNameInput(generateOutputFilename());
return;
}
setMessage('Generating output...');
// Get only file paths (not directories)
const selectedPaths = [];
const collectFilePaths = items => {
for (const item of items) {
if (item.type === 'file' && selectedFiles.has(item.path)) {
selectedPaths.push(item.path);
}
if (item.children) {
collectFilePaths(item.children);
}
}
};
collectFilePaths(items);
// Generate output with all items for tree
const output = await generateOutput({
selectedFiles: selectedPaths,
includeContent: true,
includeTree: includeFullTree,
allFiles: includeFullTree ? items : null
});
if (type === 'clipboard') {
const success = await copyToClipboard(output);
if (success) {
setMessage(`✅ Copied to clipboard (${selectedPaths.length} files)`);
setTimeout(() => onBack(), 2000);
} else {
setMessage('❌ Failed to copy to clipboard');
}
} else if (type === 'file') {
const filename = fileNameInput || generateOutputFilename();
try {
await fs.writeFile(filename, output, 'utf8');
setMessage(`✅ Saved to ${filename}`);
setIsTypingFileName(false);
setTimeout(() => onBack(), 2000);
} catch (error) {
setMessage(`❌ Failed to save: ${error.message}`);
}
}
};
useInput((input, key) => {
// Gérer ESC pendant la saisie du nom de fichier
if (isTypingFileName && key.escape) {
setIsTypingFileName(false);
return;
}
if (mode === 'output' && !isTypingFileName) {
if (key.escape || input === 'q') {
setMode('select');
setMessage('');
} else if (key.upArrow) {
setExportOption(Math.max(0, exportOption - 1));
} else if (key.downArrow) {
setExportOption(Math.min(2, exportOption + 1));
} else if (key.return) {
if (exportOption === 2) {
setIncludeFullTree(!includeFullTree);
} else {
handleExport(exportOption === 0 ? 'clipboard' : 'file');
}
}
}
});
if (isLoading) {
return /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
color: "yellow"
}, "Loading files..."));
}
if (mode === 'preset') {
return /*#__PURE__*/React.createElement(PresetManager, {
selectedFiles: selectedFiles,
onApply: handleApplyPreset,
onBack: () => setMode('select')
});
}
if (mode === 'output' && isTypingFileName) {
return /*#__PURE__*/React.createElement(Box, {
flexDirection: "column"
}, /*#__PURE__*/React.createElement(Text, {
color: "green",
bold: true
}, "\uD83D\uDCC4 Save to File"), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, null, "Enter filename (or press Enter for default):"), /*#__PURE__*/React.createElement(TextInput, {
value: fileNameInput,
onChange: setFileNameInput,
onSubmit: () => handleExport('file')
}), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, "Press ESC to cancel"));
}
if (mode === 'output') {
const options = [{
label: '📋 Copy to clipboard',
value: 'clipboard'
}, {
label: '📄 Save to file',
value: 'file'
}, {
label: includeFullTree ? '🌳 Full tree: ON' : '🌳 Full tree: OFF',
value: 'toggle-tree'
}];
return /*#__PURE__*/React.createElement(Box, {
flexDirection: "column"
}, /*#__PURE__*/React.createElement(Text, {
color: "green",
bold: true
}, "\uD83D\uDCE4 Export Options"), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, null, "Selected: ", selectedFiles.size, " items"), /*#__PURE__*/React.createElement(Text, null, " "), options.map((option, index) => /*#__PURE__*/React.createElement(Text, {
key: option.value,
color: index === exportOption ? 'cyan' : 'white'
}, index === exportOption ? '▶ ' : ' ', option.label)), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
color: "gray"
}, "\u2191\u2193 Navigate | \u23CE Select/Toggle | ESC Back"), /*#__PURE__*/React.createElement(Text, null, " "), message && /*#__PURE__*/React.createElement(Text, {
color: "yellow"
}, message));
}
return /*#__PURE__*/React.createElement(FileSelector, {
items: items,
selectedFiles: selectedFiles,
onSelectionChange: handleSelectionChange,
onBack: onBack,
onExport: () => setMode('output'),
onOpenPresets: () => setMode('preset')
});
}