canvaslms-cli 1.4.2 → 1.4.4
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/LICENSE +1 -1
- package/README.md +60 -134
- package/commands/submit.js +484 -245
- package/lib/interactive.js +416 -316
- package/package.json +3 -3
package/lib/interactive.js
CHANGED
|
@@ -8,6 +8,19 @@ import path from 'path';
|
|
|
8
8
|
import AdmZip from 'adm-zip';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
|
|
11
|
+
// Key codes for raw terminal input
|
|
12
|
+
const KEYS = {
|
|
13
|
+
UP: '\u001b[A',
|
|
14
|
+
DOWN: '\u001b[B',
|
|
15
|
+
LEFT: '\u001b[D',
|
|
16
|
+
RIGHT: '\u001b[C',
|
|
17
|
+
SPACE: ' ',
|
|
18
|
+
ENTER: '\r',
|
|
19
|
+
ESCAPE: '\u001b',
|
|
20
|
+
BACKSPACE: '\u007f',
|
|
21
|
+
TAB: '\t'
|
|
22
|
+
};
|
|
23
|
+
|
|
11
24
|
/**
|
|
12
25
|
* Create readline interface for user input
|
|
13
26
|
*/
|
|
@@ -50,16 +63,38 @@ function askQuestionWithValidation(rl, question, validator, errorMessage) {
|
|
|
50
63
|
/**
|
|
51
64
|
* Ask for confirmation (Y/n format)
|
|
52
65
|
*/
|
|
53
|
-
async function askConfirmation(rl, question, defaultYes = true) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
async function askConfirmation(rl, question, defaultYes = true, options = {}) {
|
|
67
|
+
const { requireExplicit = false } = options;
|
|
68
|
+
const suffix = defaultYes ? " (Y/n)" : " (y/N)";
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
const answer = await askQuestion(rl, question + suffix + ": ");
|
|
72
|
+
const lower = answer.toLowerCase();
|
|
73
|
+
|
|
74
|
+
// If user presses Enter → return defaultYes (true by default)
|
|
75
|
+
if (lower === "") {
|
|
76
|
+
if (!requireExplicit) {
|
|
77
|
+
return defaultYes;
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Convert input to boolean
|
|
84
|
+
if (lower === "y" || lower === "yes") {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (lower === "n" || lower === "no") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!requireExplicit) {
|
|
92
|
+
// If input is something else, fallback to defaultYes
|
|
93
|
+
return defaultYes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(chalk.yellow('Please enter "y" or "n" to confirm.'));
|
|
59
97
|
}
|
|
60
|
-
|
|
61
|
-
const lower = answer.toLowerCase();
|
|
62
|
-
return lower === 'y' || lower === 'yes';
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
/**
|
|
@@ -360,58 +395,72 @@ async function selectFilesImproved(rl, currentDir = process.cwd()) {
|
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
/**
|
|
363
|
-
*
|
|
398
|
+
* Interactive file selector with tree view and keyboard navigation
|
|
364
399
|
*/
|
|
365
|
-
async function
|
|
400
|
+
async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
|
|
366
401
|
const selectedFiles = [];
|
|
367
|
-
let
|
|
402
|
+
let expandedFolders = new Set();
|
|
403
|
+
let fileTree = [];
|
|
368
404
|
let fileList = [];
|
|
369
|
-
let
|
|
370
|
-
|
|
405
|
+
let currentPath = currentDir;
|
|
406
|
+
let currentIndex = 0;
|
|
407
|
+
let isNavigating = true;
|
|
408
|
+
let viewStartIndex = 0;
|
|
409
|
+
const maxVisibleItems = 15;
|
|
371
410
|
|
|
372
|
-
//
|
|
373
|
-
|
|
411
|
+
// Setup raw mode for keyboard input
|
|
412
|
+
process.stdin.setRawMode(true);
|
|
413
|
+
process.stdin.resume();
|
|
414
|
+
process.stdin.setEncoding('utf8');
|
|
415
|
+
|
|
416
|
+
// Helper function to build file tree
|
|
417
|
+
function buildFileTree(basePath = currentDir, level = 0, parentPath = '') {
|
|
418
|
+
const tree = [];
|
|
374
419
|
try {
|
|
375
|
-
const entries = fs.readdirSync(
|
|
376
|
-
fileList = [];
|
|
377
|
-
|
|
378
|
-
// Add parent directory option if not at root
|
|
379
|
-
if (currentPath !== currentDir) {
|
|
380
|
-
fileList.push({
|
|
381
|
-
name: '📁 .. (Parent Directory)',
|
|
382
|
-
path: path.dirname(currentPath),
|
|
383
|
-
type: 'parent',
|
|
384
|
-
size: 0
|
|
385
|
-
});
|
|
386
|
-
}
|
|
420
|
+
const entries = fs.readdirSync(basePath).sort();
|
|
387
421
|
|
|
388
422
|
// Add directories first
|
|
389
423
|
entries.forEach(entry => {
|
|
390
|
-
const fullPath = path.join(
|
|
424
|
+
const fullPath = path.join(basePath, entry);
|
|
425
|
+
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
426
|
+
|
|
391
427
|
try {
|
|
392
428
|
const stats = fs.statSync(fullPath);
|
|
393
|
-
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode'].includes(entry)) {
|
|
394
|
-
|
|
395
|
-
|
|
429
|
+
if (stats.isDirectory() && !['node_modules', '.git', 'dist', 'build', '.vscode', '.next'].includes(entry)) {
|
|
430
|
+
const isExpanded = expandedFolders.has(fullPath);
|
|
431
|
+
tree.push({
|
|
432
|
+
name: entry,
|
|
396
433
|
path: fullPath,
|
|
434
|
+
relativePath,
|
|
397
435
|
type: 'directory',
|
|
436
|
+
level,
|
|
437
|
+
isExpanded,
|
|
398
438
|
size: 0
|
|
399
439
|
});
|
|
440
|
+
|
|
441
|
+
// If expanded, add children
|
|
442
|
+
if (isExpanded) {
|
|
443
|
+
const children = buildFileTree(fullPath, level + 1, relativePath);
|
|
444
|
+
tree.push(...children);
|
|
445
|
+
}
|
|
400
446
|
}
|
|
401
447
|
} catch (e) {}
|
|
402
448
|
});
|
|
403
449
|
|
|
404
450
|
// Add files
|
|
405
451
|
entries.forEach(entry => {
|
|
406
|
-
const fullPath = path.join(
|
|
452
|
+
const fullPath = path.join(basePath, entry);
|
|
453
|
+
const relativePath = parentPath ? `${parentPath}/${entry}` : entry;
|
|
454
|
+
|
|
407
455
|
try {
|
|
408
456
|
const stats = fs.statSync(fullPath);
|
|
409
457
|
if (stats.isFile()) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
name: `${icon} ${entry}`,
|
|
458
|
+
tree.push({
|
|
459
|
+
name: entry,
|
|
413
460
|
path: fullPath,
|
|
461
|
+
relativePath,
|
|
414
462
|
type: 'file',
|
|
463
|
+
level,
|
|
415
464
|
size: stats.size
|
|
416
465
|
});
|
|
417
466
|
}
|
|
@@ -420,6 +469,8 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
420
469
|
} catch (error) {
|
|
421
470
|
console.log(chalk.red('Error reading directory: ' + error.message));
|
|
422
471
|
}
|
|
472
|
+
|
|
473
|
+
return tree;
|
|
423
474
|
}
|
|
424
475
|
|
|
425
476
|
// Helper function to get file icon based on extension
|
|
@@ -437,19 +488,43 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
437
488
|
};
|
|
438
489
|
return icons[ext] || '📋';
|
|
439
490
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
console.log(chalk.cyan.bold('╰' + '─'.repeat(78) + '╯'));
|
|
491
|
+
// Helper function to build breadcrumb path
|
|
492
|
+
function buildBreadcrumb() {
|
|
493
|
+
const relativePath = path.relative(currentDir, currentPath);
|
|
494
|
+
if (!relativePath || relativePath === '.') {
|
|
495
|
+
return ''
|
|
496
|
+
}
|
|
447
497
|
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
498
|
+
const parts = relativePath.split(path.sep);
|
|
499
|
+
const breadcrumb = parts.map((part, index) => {
|
|
500
|
+
if (index === parts.length - 1) {
|
|
501
|
+
return chalk.white.bold(part);
|
|
502
|
+
}
|
|
503
|
+
return chalk.gray(part);
|
|
504
|
+
}).join(chalk.gray(' › '));
|
|
505
|
+
|
|
506
|
+
return chalk.yellow('📂 ') + breadcrumb;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Helper to clear the terminal without dropping scrollback history
|
|
510
|
+
function safeClearScreen() {
|
|
511
|
+
if (!process.stdout.isTTY) return;
|
|
512
|
+
|
|
513
|
+
process.stdout.write('\x1b[H\x1b[J'); // Move cursor to top and clear below, not full console.clear()
|
|
514
|
+
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Helper function to display the file browser
|
|
518
|
+
function displayBrowser() {
|
|
519
|
+
safeClearScreen();
|
|
520
|
+
// Keyboard controls - compact format at top
|
|
521
|
+
const controls = [
|
|
522
|
+
];
|
|
523
|
+
// Breadcrumb path
|
|
524
|
+
console.log(buildBreadcrumb());
|
|
525
|
+
console.log(chalk.gray('💡 ↑↓←→:Navigate', 'Space:Select', 'Enter:Open/Finish', 'Backspace:Up', 'a:All', 'c:Clear', 'Esc:Exit'));
|
|
451
526
|
|
|
452
|
-
//
|
|
527
|
+
// Selected files count
|
|
453
528
|
if (selectedFiles.length > 0) {
|
|
454
529
|
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
455
530
|
try {
|
|
@@ -458,321 +533,346 @@ async function selectFilesInteractive(rl, currentDir = process.cwd()) {
|
|
|
458
533
|
return sum;
|
|
459
534
|
}
|
|
460
535
|
}, 0);
|
|
461
|
-
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB)`));
|
|
536
|
+
console.log(chalk.green(`✅ Selected: ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB) - Press Enter to finish`));
|
|
462
537
|
}
|
|
463
|
-
|
|
538
|
+
|
|
464
539
|
console.log();
|
|
465
|
-
|
|
540
|
+
|
|
466
541
|
if (fileList.length === 0) {
|
|
467
542
|
console.log(chalk.yellow('📭 No files found in this directory.'));
|
|
468
543
|
return;
|
|
469
544
|
}
|
|
470
545
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
546
|
+
// Display files with tree-like structure
|
|
547
|
+
displayFileTree();
|
|
548
|
+
}
|
|
549
|
+
// Helper function to display files in a horizontal grid layout
|
|
550
|
+
function displayFileTree() {
|
|
551
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
552
|
+
const maxDisplayItems = 50; // Show more items in grid view
|
|
553
|
+
const startIdx = Math.max(0, currentIndex - Math.floor(maxDisplayItems / 2));
|
|
554
|
+
const endIdx = Math.min(fileList.length, startIdx + maxDisplayItems);
|
|
555
|
+
const visibleItems = fileList.slice(startIdx, endIdx);
|
|
478
556
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const prefix = isSelected ? chalk.green('✓') : chalk.white(globalIndex.toString().padStart(2));
|
|
484
|
-
const sizeDisplay = item.type === 'file' ? chalk.gray(`(${(item.size / 1024).toFixed(1)} KB)`) : '';
|
|
485
|
-
|
|
486
|
-
if (isSelected) {
|
|
487
|
-
console.log(`${prefix} ${chalk.green(item.name)} ${sizeDisplay}`);
|
|
488
|
-
} else if (item.type === 'parent') {
|
|
489
|
-
console.log(`${prefix} ${chalk.blue(item.name)}`);
|
|
490
|
-
} else if (item.type === 'directory') {
|
|
491
|
-
console.log(`${prefix} ${chalk.cyan(item.name)}`);
|
|
492
|
-
} else {
|
|
493
|
-
console.log(`${prefix} ${chalk.white(item.name)} ${sizeDisplay}`);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
557
|
+
// Show scroll indicators
|
|
558
|
+
if (startIdx > 0) {
|
|
559
|
+
console.log(chalk.gray(` ⋮ (${startIdx} items above)`));
|
|
560
|
+
}
|
|
496
561
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
console.log(chalk.yellow.bold('Commands:'));
|
|
503
|
-
console.log(chalk.white(' 1-9, 10+ ') + chalk.gray('Select/deselect file by number'));
|
|
504
|
-
console.log(chalk.white(' n/next/→ ') + chalk.gray('Next page'));
|
|
505
|
-
console.log(chalk.white(' p/prev/← ') + chalk.gray('Previous page'));
|
|
506
|
-
console.log(chalk.white(' a/all ') + chalk.gray('Select all files on current page'));
|
|
507
|
-
console.log(chalk.white(' c/clear ') + chalk.gray('Clear all selections'));
|
|
508
|
-
console.log(chalk.white(' r/remove ') + chalk.gray('Remove specific file from selection'));
|
|
509
|
-
console.log(chalk.white(' s/search ') + chalk.gray('Search files by name or extension'));
|
|
510
|
-
console.log(chalk.white(' f/filter ') + chalk.gray('Filter by file extension'));
|
|
511
|
-
console.log(chalk.white(' h/help ') + chalk.gray('Show detailed help'));
|
|
512
|
-
console.log(chalk.white(' q/quit ') + chalk.gray('Finish selection'));
|
|
513
|
-
console.log();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Main interaction loop
|
|
517
|
-
while (true) {
|
|
518
|
-
refreshFileList();
|
|
519
|
-
displayPage();
|
|
562
|
+
// Calculate item width and columns
|
|
563
|
+
const maxItemWidth = Math.max(...visibleItems.map(item => {
|
|
564
|
+
const name = path.basename(item.path);
|
|
565
|
+
return name.length + 4; // 2 for icon + space, 2 for padding
|
|
566
|
+
}));
|
|
520
567
|
|
|
521
|
-
const
|
|
522
|
-
const
|
|
568
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25); // Min 15, max 25 chars
|
|
569
|
+
const columnsPerRow = Math.floor((terminalWidth - 4) / itemWidth); // Leave 4 chars margin
|
|
570
|
+
const actualColumns = Math.max(1, columnsPerRow);
|
|
523
571
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// Handle navigation
|
|
528
|
-
if (cmd === 'n' || cmd === 'next' || cmd === '→') {
|
|
529
|
-
if ((currentPage + 1) * itemsPerPage < fileList.length) {
|
|
530
|
-
currentPage++;
|
|
531
|
-
} else {
|
|
532
|
-
console.log(chalk.yellow('Already on last page!'));
|
|
533
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
534
|
-
}
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
572
|
+
// Group items into rows
|
|
573
|
+
let currentRow = '';
|
|
574
|
+
let itemsInCurrentRow = 0;
|
|
537
575
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
576
|
+
visibleItems.forEach((item, index) => {
|
|
577
|
+
const actualIndex = startIdx + index;
|
|
578
|
+
const isSelected = selectedFiles.includes(item.path);
|
|
579
|
+
const isCurrent = actualIndex === currentIndex;
|
|
580
|
+
|
|
581
|
+
// Get icon and name
|
|
582
|
+
let icon = '';
|
|
583
|
+
if (item.type === 'parent' || item.type === 'directory') {
|
|
584
|
+
icon = '📁';
|
|
541
585
|
} else {
|
|
542
|
-
|
|
543
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
586
|
+
icon = getFileIcon(path.basename(item.path));
|
|
544
587
|
}
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Handle help
|
|
549
|
-
if (cmd === 'h' || cmd === 'help') {
|
|
550
|
-
console.clear();
|
|
551
|
-
console.log(chalk.cyan.bold('╭' + '─'.repeat(78) + '╮'));
|
|
552
|
-
console.log(chalk.cyan.bold('│') + chalk.white.bold(' Interactive File Selector - Help'.padEnd(76)) + chalk.cyan.bold('│'));
|
|
553
|
-
console.log(chalk.cyan.bold('╰' + '─'.repeat(78) + '╯'));
|
|
554
|
-
console.log();
|
|
555
|
-
console.log(chalk.yellow.bold('📋 File Selection:'));
|
|
556
|
-
console.log(chalk.white(' • Type a number (1-9999) to toggle file selection'));
|
|
557
|
-
console.log(chalk.white(' • Files show with checkmarks (✓) when selected'));
|
|
558
|
-
console.log(chalk.white(' • Directories and parent folders can be navigated into'));
|
|
559
|
-
console.log();
|
|
560
|
-
console.log(chalk.yellow.bold('📁 Navigation:'));
|
|
561
|
-
console.log(chalk.white(' • Use n/next/→ to go to next page'));
|
|
562
|
-
console.log(chalk.white(' • Use p/prev/← to go to previous page'));
|
|
563
|
-
console.log(chalk.white(' • Click on folder numbers to enter directories'));
|
|
564
|
-
console.log(chalk.white(' • Use .. (Parent Directory) to go up one level'));
|
|
565
|
-
console.log();
|
|
566
|
-
console.log(chalk.yellow.bold('🔍 Advanced Features:'));
|
|
567
|
-
console.log(chalk.white(' • s/search: Find files by name or extension'));
|
|
568
|
-
console.log(chalk.white(' • f/filter: Filter and select by file extension'));
|
|
569
|
-
console.log(chalk.white(' • a/all: Select all files on current page'));
|
|
570
|
-
console.log(chalk.white(' • c/clear: Remove all selections'));
|
|
571
|
-
console.log(chalk.white(' • r/remove: Remove specific files from selection'));
|
|
572
|
-
console.log();
|
|
573
|
-
console.log(chalk.yellow.bold('💡 Tips:'));
|
|
574
|
-
console.log(chalk.white(' • File sizes are shown in KB'));
|
|
575
|
-
console.log(chalk.white(' • Your current selection count and size is displayed at top'));
|
|
576
|
-
console.log(chalk.white(' • Use search for large directories to find files quickly'));
|
|
577
|
-
console.log(chalk.white(' • Filter by extension (e.g., .pdf, .docx) for specific file types'));
|
|
578
|
-
console.log();
|
|
579
|
-
console.log(chalk.green('Press Enter to continue...'));
|
|
580
|
-
await askQuestion(rl, '');
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Handle selection commands
|
|
585
|
-
if (cmd === 'a' || cmd === 'all') {
|
|
586
|
-
const startIdx = currentPage * itemsPerPage;
|
|
587
|
-
const endIdx = Math.min(startIdx + itemsPerPage, fileList.length);
|
|
588
|
-
let addedCount = 0;
|
|
589
588
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
589
|
+
const name = item.name || path.basename(item.path);
|
|
590
|
+
const truncatedName = name.length > itemWidth - 4 ? name.slice(0, itemWidth - 7) + '...' : name;
|
|
591
|
+
|
|
592
|
+
// Build item display string
|
|
593
|
+
let itemDisplay = `${icon} ${truncatedName}`;
|
|
594
|
+
|
|
595
|
+
// Apply styling based on state
|
|
596
|
+
if (isCurrent) {
|
|
597
|
+
if (isSelected) {
|
|
598
|
+
itemDisplay = chalk.black.bgGreen(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
599
|
+
} else if (item.type === 'parent') {
|
|
600
|
+
itemDisplay = chalk.white.bgBlue(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
601
|
+
} else if (item.type === 'directory') {
|
|
602
|
+
itemDisplay = chalk.black.bgCyan(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
603
|
+
} else {
|
|
604
|
+
itemDisplay = chalk.black.bgWhite(` ${itemDisplay}`.padEnd(itemWidth - 1));
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
if (isSelected) {
|
|
608
|
+
itemDisplay = chalk.green(`✓${itemDisplay}`.padEnd(itemWidth));
|
|
609
|
+
} else if (item.type === 'parent') {
|
|
610
|
+
itemDisplay = chalk.blue(` ${itemDisplay}`.padEnd(itemWidth));
|
|
611
|
+
} else if (item.type === 'directory') {
|
|
612
|
+
itemDisplay = chalk.cyan(` ${itemDisplay}`.padEnd(itemWidth));
|
|
613
|
+
} else {
|
|
614
|
+
itemDisplay = chalk.white(` ${itemDisplay}`.padEnd(itemWidth));
|
|
595
615
|
}
|
|
596
616
|
}
|
|
597
617
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
618
|
+
// Add to current row
|
|
619
|
+
currentRow += itemDisplay;
|
|
620
|
+
itemsInCurrentRow++;
|
|
621
|
+
|
|
622
|
+
// Check if we need to start a new row
|
|
623
|
+
if (itemsInCurrentRow >= actualColumns || index === visibleItems.length - 1) {
|
|
624
|
+
console.log(currentRow);
|
|
625
|
+
currentRow = '';
|
|
626
|
+
itemsInCurrentRow = 0;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Show scroll indicators
|
|
631
|
+
if (endIdx < fileList.length) {
|
|
632
|
+
console.log(chalk.gray(` ⋮ (${fileList.length - endIdx} items below)`));
|
|
601
633
|
}
|
|
602
634
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
635
|
+
console.log();
|
|
636
|
+
|
|
637
|
+
// Show current position and navigation info
|
|
638
|
+
if (fileList.length > maxDisplayItems) {
|
|
639
|
+
console.log(chalk.gray(`Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
640
|
+
} else {
|
|
641
|
+
console.log(chalk.gray(`${fileList.length} items | Current: ${currentIndex + 1}`));
|
|
609
642
|
}
|
|
610
643
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
644
|
+
// Show grid info
|
|
645
|
+
console.log(chalk.gray(`Grid: ${actualColumns} columns × ${itemWidth} chars | Terminal width: ${terminalWidth}`));
|
|
646
|
+
}
|
|
647
|
+
// Helper function to refresh file list for current directory
|
|
648
|
+
function refreshFileList() {
|
|
649
|
+
fileList = [];
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
// Add parent directory option if not at root
|
|
653
|
+
if (currentPath !== currentDir) {
|
|
654
|
+
fileList.push({
|
|
655
|
+
type: 'parent',
|
|
656
|
+
path: path.dirname(currentPath),
|
|
657
|
+
name: '..'
|
|
658
|
+
});
|
|
616
659
|
}
|
|
617
660
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
661
|
+
// Read current directory
|
|
662
|
+
const entries = fs.readdirSync(currentPath).sort();
|
|
663
|
+
|
|
664
|
+
// Add directories first
|
|
665
|
+
entries.forEach(entry => {
|
|
666
|
+
const fullPath = path.join(currentPath, entry);
|
|
667
|
+
const stat = fs.statSync(fullPath);
|
|
668
|
+
|
|
669
|
+
if (stat.isDirectory()) {
|
|
670
|
+
fileList.push({
|
|
671
|
+
type: 'directory',
|
|
672
|
+
path: fullPath,
|
|
673
|
+
name: entry
|
|
674
|
+
});
|
|
675
|
+
}
|
|
621
676
|
});
|
|
622
677
|
|
|
623
|
-
|
|
624
|
-
|
|
678
|
+
// Add files
|
|
679
|
+
entries.forEach(entry => {
|
|
680
|
+
const fullPath = path.join(currentPath, entry);
|
|
681
|
+
const stat = fs.statSync(fullPath);
|
|
682
|
+
|
|
683
|
+
if (stat.isFile()) {
|
|
684
|
+
fileList.push({
|
|
685
|
+
type: 'file',
|
|
686
|
+
path: fullPath,
|
|
687
|
+
name: entry,
|
|
688
|
+
size: stat.size
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
});
|
|
625
692
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
693
|
+
// Ensure currentIndex is within bounds
|
|
694
|
+
if (currentIndex >= fileList.length) {
|
|
695
|
+
currentIndex = Math.max(0, fileList.length - 1);
|
|
629
696
|
}
|
|
630
697
|
|
|
631
|
-
|
|
632
|
-
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.error('Error reading directory:', error.message);
|
|
700
|
+
fileList = [];
|
|
633
701
|
}
|
|
702
|
+
}
|
|
703
|
+
// Main keyboard event handler
|
|
704
|
+
function handleKeyInput(key) {
|
|
705
|
+
// Calculate grid dimensions for navigation
|
|
706
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
707
|
+
const maxItemWidth = Math.max(...fileList.map(item => {
|
|
708
|
+
const name = path.basename(item.path);
|
|
709
|
+
return name.length + 4;
|
|
710
|
+
}));
|
|
711
|
+
const itemWidth = Math.min(Math.max(maxItemWidth, 15), 25);
|
|
712
|
+
const columnsPerRow = Math.max(1, Math.floor((terminalWidth - 4) / itemWidth));
|
|
634
713
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
714
|
+
switch (key) {
|
|
715
|
+
case KEYS.UP:
|
|
716
|
+
// Move up by one row (subtract columns)
|
|
717
|
+
const newUpIndex = currentIndex - columnsPerRow;
|
|
718
|
+
if (newUpIndex >= 0) {
|
|
719
|
+
currentIndex = newUpIndex;
|
|
720
|
+
displayBrowser();
|
|
721
|
+
}
|
|
722
|
+
break;
|
|
643
723
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
724
|
+
case KEYS.DOWN:
|
|
725
|
+
// Move down by one row (add columns)
|
|
726
|
+
const newDownIndex = currentIndex + columnsPerRow;
|
|
727
|
+
if (newDownIndex < fileList.length) {
|
|
728
|
+
currentIndex = newDownIndex;
|
|
729
|
+
displayBrowser();
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
|
|
733
|
+
case KEYS.LEFT:
|
|
734
|
+
// Move left by one column
|
|
735
|
+
if (currentIndex > 0) {
|
|
736
|
+
currentIndex--;
|
|
737
|
+
displayBrowser();
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
|
|
741
|
+
case KEYS.RIGHT:
|
|
742
|
+
// Move right by one column
|
|
743
|
+
if (currentIndex < fileList.length - 1) {
|
|
744
|
+
currentIndex++;
|
|
745
|
+
displayBrowser();
|
|
746
|
+
}
|
|
747
|
+
break;
|
|
748
|
+
|
|
749
|
+
case KEYS.SPACE:
|
|
750
|
+
if (fileList.length > 0) {
|
|
751
|
+
const item = fileList[currentIndex];
|
|
752
|
+
if (item.type === 'file') {
|
|
753
|
+
const index = selectedFiles.indexOf(item.path);
|
|
754
|
+
if (index === -1) {
|
|
755
|
+
selectedFiles.push(item.path);
|
|
756
|
+
} else {
|
|
757
|
+
selectedFiles.splice(index, 1);
|
|
758
|
+
}
|
|
759
|
+
displayBrowser();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
break;
|
|
763
|
+
case KEYS.ENTER:
|
|
764
|
+
if (fileList.length > 0) {
|
|
765
|
+
const item = fileList[currentIndex];
|
|
766
|
+
if (item.type === 'parent' || item.type === 'directory') {
|
|
767
|
+
currentPath = item.path;
|
|
768
|
+
currentIndex = 0;
|
|
769
|
+
refreshFileList();
|
|
770
|
+
displayBrowser();
|
|
771
|
+
} else {
|
|
772
|
+
// When Enter is pressed on a file, finish selection if files are selected
|
|
773
|
+
if (selectedFiles.length > 0) {
|
|
774
|
+
isNavigating = false;
|
|
775
|
+
}
|
|
660
776
|
}
|
|
661
777
|
} else {
|
|
662
|
-
|
|
778
|
+
// Finish selection when directory is empty and Enter is pressed (if files selected)
|
|
779
|
+
if (selectedFiles.length > 0) {
|
|
780
|
+
isNavigating = false;
|
|
781
|
+
}
|
|
663
782
|
}
|
|
783
|
+
break;
|
|
664
784
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
785
|
+
case KEYS.BACKSPACE:
|
|
786
|
+
if (currentPath !== currentDir) {
|
|
787
|
+
currentPath = path.dirname(currentPath);
|
|
788
|
+
currentIndex = 0;
|
|
789
|
+
refreshFileList();
|
|
790
|
+
displayBrowser();
|
|
791
|
+
}
|
|
792
|
+
break;
|
|
793
|
+
|
|
794
|
+
case 'a':
|
|
795
|
+
// Select all files in current directory
|
|
796
|
+
let addedCount = 0;
|
|
797
|
+
fileList.forEach(item => {
|
|
798
|
+
if (item.type === 'file' && !selectedFiles.includes(item.path)) {
|
|
799
|
+
selectedFiles.push(item.path);
|
|
800
|
+
addedCount++;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
if (addedCount > 0) {
|
|
804
|
+
displayBrowser();
|
|
805
|
+
}
|
|
806
|
+
break;
|
|
807
|
+
|
|
808
|
+
case 'c':
|
|
809
|
+
// Clear all selections
|
|
810
|
+
selectedFiles.length = 0;
|
|
811
|
+
displayBrowser();
|
|
812
|
+
break;
|
|
813
|
+
|
|
814
|
+
case KEYS.ESCAPE:
|
|
815
|
+
case '\u001b': // ESC key
|
|
816
|
+
isNavigating = false;
|
|
817
|
+
break;
|
|
818
|
+
|
|
819
|
+
default:
|
|
820
|
+
// Ignore other keys
|
|
821
|
+
break;
|
|
668
822
|
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Initialize and start navigation
|
|
826
|
+
return new Promise((resolve) => {
|
|
827
|
+
refreshFileList();
|
|
828
|
+
displayBrowser();
|
|
669
829
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
.filter(item => item.type === 'file')
|
|
674
|
-
.map(item => path.extname(item.path).toLowerCase())
|
|
675
|
-
.filter(ext => ext)
|
|
676
|
-
)].sort();
|
|
677
|
-
|
|
678
|
-
if (allExtensions.length === 0) {
|
|
679
|
-
console.log(chalk.yellow('No file extensions found!'));
|
|
680
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
681
|
-
continue;
|
|
830
|
+
process.stdin.on('data', (key) => {
|
|
831
|
+
if (!isNavigating) {
|
|
832
|
+
return;
|
|
682
833
|
}
|
|
683
834
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const count = fileList.filter(item =>
|
|
687
|
-
item.type === 'file' && path.extname(item.path).toLowerCase() === ext
|
|
688
|
-
).length;
|
|
689
|
-
console.log(`${index + 1}. ${ext} (${count} files)`);
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
const extChoice = await askQuestion(rl, chalk.cyan('Enter extension number (or press Enter to cancel): '));
|
|
693
|
-
const extIndex = parseInt(extChoice) - 1;
|
|
835
|
+
const keyStr = key.toString();
|
|
836
|
+
handleKeyInput(keyStr);
|
|
694
837
|
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
838
|
+
if (!isNavigating) {
|
|
839
|
+
// Cleanup
|
|
840
|
+
process.stdin.removeAllListeners('data');
|
|
841
|
+
process.stdin.setRawMode(false);
|
|
842
|
+
process.stdin.pause();
|
|
700
843
|
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
844
|
+
// Display completion summary
|
|
845
|
+
if (selectedFiles.length > 0) {
|
|
846
|
+
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
847
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
848
|
+
|
|
849
|
+
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
850
|
+
try {
|
|
851
|
+
return sum + fs.statSync(file).size;
|
|
852
|
+
} catch {
|
|
853
|
+
return sum;
|
|
854
|
+
}
|
|
855
|
+
}, 0);
|
|
856
|
+
|
|
857
|
+
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
858
|
+
selectedFiles.forEach((file, index) => {
|
|
859
|
+
try {
|
|
860
|
+
const stats = fs.statSync(file);
|
|
861
|
+
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
862
|
+
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
863
|
+
} catch (e) {
|
|
864
|
+
console.log(pad(chalk.red(`${index + 1}.`), 5) + chalk.red(path.basename(file) + ' (Error reading file)'));
|
|
706
865
|
}
|
|
707
866
|
});
|
|
708
|
-
console.log(chalk.
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
713
|
-
continue;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Handle numeric selection
|
|
717
|
-
const num = parseInt(cmd);
|
|
718
|
-
if (!isNaN(num) && num > 0 && num <= fileList.length) {
|
|
719
|
-
const item = fileList[num - 1];
|
|
720
|
-
|
|
721
|
-
if (item.type === 'parent') {
|
|
722
|
-
currentPath = item.path;
|
|
723
|
-
currentPage = 0;
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (item.type === 'directory') {
|
|
728
|
-
currentPath = item.path;
|
|
729
|
-
currentPage = 0;
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
if (item.type === 'file') {
|
|
733
|
-
const index = selectedFiles.indexOf(item.path);
|
|
734
|
-
if (index === -1) {
|
|
735
|
-
selectedFiles.push(item.path);
|
|
736
|
-
const size = (item.size / 1024).toFixed(1);
|
|
737
|
-
console.log(chalk.green(`✓ Added: ${path.basename(item.path)} (${size} KB)`));
|
|
867
|
+
console.log(chalk.cyan('-'.repeat(50)));
|
|
738
868
|
} else {
|
|
739
|
-
|
|
740
|
-
console.log(chalk.yellow(`✗ Removed: ${path.basename(item.path)}`));
|
|
869
|
+
console.log(chalk.yellow('No files selected.'));
|
|
741
870
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
console.log(chalk.red('Invalid command!'));
|
|
747
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Display completion summary
|
|
751
|
-
if (selectedFiles.length > 0) {
|
|
752
|
-
console.clear();
|
|
753
|
-
console.log(chalk.green.bold('✅ File Selection Complete!'));
|
|
754
|
-
console.log(chalk.cyan('-'.repeat(50)));
|
|
755
|
-
|
|
756
|
-
const totalSize = selectedFiles.reduce((sum, file) => {
|
|
757
|
-
try {
|
|
758
|
-
return sum + fs.statSync(file).size;
|
|
759
|
-
} catch {
|
|
760
|
-
return sum;
|
|
871
|
+
|
|
872
|
+
resolve(selectedFiles);
|
|
761
873
|
}
|
|
762
|
-
}, 0);
|
|
763
|
-
|
|
764
|
-
console.log(chalk.white(`Selected ${selectedFiles.length} files (${(totalSize / 1024).toFixed(1)} KB total):`));
|
|
765
|
-
selectedFiles.forEach((file, index) => {
|
|
766
|
-
const stats = fs.statSync(file);
|
|
767
|
-
const size = (stats.size / 1024).toFixed(1) + ' KB';
|
|
768
|
-
console.log(pad(chalk.green(`${index + 1}.`), 5) + pad(path.basename(file), 35) + chalk.gray(size));
|
|
769
874
|
});
|
|
770
|
-
|
|
771
|
-
} else {
|
|
772
|
-
console.log(chalk.yellow('No files selected.'));
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
return selectedFiles;
|
|
875
|
+
});
|
|
776
876
|
}
|
|
777
877
|
|
|
778
878
|
export {
|
|
@@ -782,8 +882,8 @@ export {
|
|
|
782
882
|
askConfirmation,
|
|
783
883
|
selectFromList,
|
|
784
884
|
selectFilesImproved,
|
|
785
|
-
|
|
885
|
+
selectFilesKeyboard,
|
|
786
886
|
getFilesMatchingWildcard,
|
|
787
887
|
getSubfoldersRecursive,
|
|
788
888
|
pad
|
|
789
|
-
};
|
|
889
|
+
};
|