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.
@@ -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 suffix = defaultYes ? ' (Y/n)' : ' (y/N)';
55
- const answer = await askQuestion(rl, question + suffix + ': ');
56
-
57
- if (answer.trim() === '') {
58
- return defaultYes;
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
- * Enhanced interactive file selector with real-time preview and navigation
398
+ * Interactive file selector with tree view and keyboard navigation
364
399
  */
365
- async function selectFilesInteractive(rl, currentDir = process.cwd()) {
400
+ async function selectFilesKeyboard(rl, currentDir = process.cwd()) {
366
401
  const selectedFiles = [];
367
- let currentPath = currentDir;
402
+ let expandedFolders = new Set();
403
+ let fileTree = [];
368
404
  let fileList = [];
369
- let currentPage = 0;
370
- const itemsPerPage = 10;
405
+ let currentPath = currentDir;
406
+ let currentIndex = 0;
407
+ let isNavigating = true;
408
+ let viewStartIndex = 0;
409
+ const maxVisibleItems = 15;
371
410
 
372
- // Helper function to refresh file list
373
- function refreshFileList() {
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(currentPath);
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(currentPath, entry);
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
- fileList.push({
395
- name: `📁 ${entry}`,
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(currentPath, entry);
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
- const icon = getFileIcon(entry);
411
- fileList.push({
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
- // Helper function to display current page
442
- function displayPage() {
443
- console.clear();
444
- console.log(chalk.cyan.bold('╭' + ''.repeat(78) + '╮'));
445
- console.log(chalk.cyan.bold('│') + chalk.white.bold(' Interactive File Selector'.padEnd(76)) + chalk.cyan.bold('│'));
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
- // Display current path
449
- const relativePath = path.relative(currentDir, currentPath) || '.';
450
- console.log(chalk.yellow('📂 Current folder: ') + chalk.white.bold(relativePath));
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
- // Display selected files count
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
- const startIdx = currentPage * itemsPerPage;
472
- const endIdx = Math.min(startIdx + itemsPerPage, fileList.length);
473
- const pageItems = fileList.slice(startIdx, endIdx);
474
-
475
- // Display page header
476
- console.log(chalk.cyan(`📋 Showing ${startIdx + 1}-${endIdx} of ${fileList.length} items (Page ${currentPage + 1}/${Math.ceil(fileList.length / itemsPerPage)})`));
477
- console.log(chalk.gray('─'.repeat(80)));
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
- // Display items
480
- pageItems.forEach((item, index) => {
481
- const globalIndex = startIdx + index + 1;
482
- const isSelected = selectedFiles.includes(item.path);
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
- console.log(chalk.gray('─'.repeat(80)));
498
- displayCommands();
499
- }
500
- // Helper function to display available commands
501
- function displayCommands() {
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 input = await askQuestion(rl, chalk.cyan.bold('Enter command: '));
522
- const cmd = input.trim().toLowerCase();
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
- if (!cmd || cmd === 'q' || cmd === 'quit') {
525
- break;
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
- if (cmd === 'p' || cmd === 'prev' || cmd === '←') {
539
- if (currentPage > 0) {
540
- currentPage--;
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
- console.log(chalk.yellow('Already on first page!'));
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
- for (let i = startIdx; i < endIdx; i++) {
591
- const item = fileList[i];
592
- if (item.type === 'file' && !selectedFiles.includes(item.path)) {
593
- selectedFiles.push(item.path);
594
- addedCount++;
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
- console.log(chalk.green(`Added ${addedCount} files to selection!`));
599
- await new Promise(resolve => setTimeout(resolve, 1000));
600
- continue;
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
- if (cmd === 'c' || cmd === 'clear') {
604
- const count = selectedFiles.length;
605
- selectedFiles.length = 0;
606
- console.log(chalk.yellow(`Cleared ${count} files from selection!`));
607
- await new Promise(resolve => setTimeout(resolve, 1000));
608
- continue;
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
- if (cmd === 'r' || cmd === 'remove') {
612
- if (selectedFiles.length === 0) {
613
- console.log(chalk.red('No files selected to remove!'));
614
- await new Promise(resolve => setTimeout(resolve, 1000));
615
- continue;
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
- console.log(chalk.cyan('\nSelected files:'));
619
- selectedFiles.forEach((file, index) => {
620
- console.log(`${index + 1}. ${path.basename(file)}`);
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
- const removeInput = await askQuestion(rl, chalk.cyan('Enter number to remove (or press Enter to cancel): '));
624
- const removeIndex = parseInt(removeInput) - 1;
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
- if (removeIndex >= 0 && removeIndex < selectedFiles.length) {
627
- const removed = selectedFiles.splice(removeIndex, 1)[0];
628
- console.log(chalk.green(`Removed: ${path.basename(removed)}`));
693
+ // Ensure currentIndex is within bounds
694
+ if (currentIndex >= fileList.length) {
695
+ currentIndex = Math.max(0, fileList.length - 1);
629
696
  }
630
697
 
631
- await new Promise(resolve => setTimeout(resolve, 1000));
632
- continue;
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
- if (cmd === 's' || cmd === 'search') {
636
- const searchTerm = await askQuestion(rl, chalk.cyan('Enter search term (filename or extension): '));
637
- if (searchTerm) {
638
- const matches = fileList.filter(item =>
639
- item.type === 'file' &&
640
- (item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
641
- path.extname(item.path).toLowerCase() === searchTerm.toLowerCase())
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
- if (matches.length > 0) {
645
- console.log(chalk.cyan(`\nFound ${matches.length} matches:`));
646
- matches.forEach((item, index) => {
647
- const isSelected = selectedFiles.includes(item.path);
648
- const prefix = isSelected ? chalk.green('✓') : chalk.white((index + 1).toString());
649
- console.log(`${prefix} ${item.name} ${chalk.gray(`(${(item.size / 1024).toFixed(1)} KB)`)}`);
650
- });
651
-
652
- const selectAll = await askConfirmation(rl, chalk.cyan('Select all search results?'), false);
653
- if (selectAll) {
654
- matches.forEach(item => {
655
- if (!selectedFiles.includes(item.path)) {
656
- selectedFiles.push(item.path);
657
- }
658
- });
659
- console.log(chalk.green(`Added ${matches.length} files!`));
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
- console.log(chalk.yellow('No matches found.'));
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
- await new Promise(resolve => setTimeout(resolve, 2000));
666
- }
667
- continue;
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
- if (cmd === 'f' || cmd === 'filter') {
671
- const allExtensions = [...new Set(
672
- fileList
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
- console.log(chalk.cyan('\nAvailable extensions:'));
685
- allExtensions.forEach((ext, index) => {
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 (extIndex >= 0 && extIndex < allExtensions.length) {
696
- const selectedExt = allExtensions[extIndex];
697
- const matches = fileList.filter(item =>
698
- item.type === 'file' && path.extname(item.path).toLowerCase() === selectedExt
699
- );
838
+ if (!isNavigating) {
839
+ // Cleanup
840
+ process.stdin.removeAllListeners('data');
841
+ process.stdin.setRawMode(false);
842
+ process.stdin.pause();
700
843
 
701
- const selectAll = await askConfirmation(rl, chalk.cyan(`Select all ${selectedExt} files?`), false);
702
- if (selectAll) {
703
- matches.forEach(item => {
704
- if (!selectedFiles.includes(item.path)) {
705
- selectedFiles.push(item.path);
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.green(`Added ${matches.length} ${selectedExt} files!`));
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
- selectedFiles.splice(index, 1);
740
- console.log(chalk.yellow(`✗ Removed: ${path.basename(item.path)}`));
869
+ console.log(chalk.yellow('No files selected.'));
741
870
  }
742
- await new Promise(resolve => setTimeout(resolve, 800));
743
- continue;
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
- console.log(chalk.cyan('-'.repeat(50)));
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
- selectFilesInteractive,
885
+ selectFilesKeyboard,
786
886
  getFilesMatchingWildcard,
787
887
  getSubfoldersRecursive,
788
888
  pad
789
- };
889
+ };