git0 0.2.10 → 0.2.12

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/src/fm.js ADDED
@@ -0,0 +1,1130 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { execSync } = require('child_process');
7
+ const os = require('os');
8
+
9
+ class FileManager {
10
+ constructor() {
11
+ this.currentPath = process.cwd();
12
+ this.selectedIndex = 0;
13
+ this.items = [];
14
+ this.showHidden = false;
15
+ this.sortBy = 'name'; // 'name', 'size', 'date', 'type'
16
+ this.sortOrder = 'asc'; // 'asc', 'desc'
17
+ this.clipboard = [];
18
+ this.clipboardAction = null; // 'copy' or 'cut'
19
+ this.viewMode = 'detailed'; // 'detailed', 'simple', 'grid'
20
+ this.filterText = '';
21
+ this.bookmarks = this.loadBookmarks();
22
+ this.history = [process.cwd()];
23
+ this.historyIndex = 0;
24
+ this.selectedItems = new Set();
25
+ this.multiSelectMode = false;
26
+ this.searchMode = false;
27
+ this.previewMode = false;
28
+ this.splitView = false;
29
+ this.rightPanePath = os.homedir();
30
+ this.activePane = 'left'; // 'left', 'right'
31
+
32
+ // File type icons
33
+ this.fileIcons = {
34
+ // Documents
35
+ '.pdf': '📄', '.doc': '📝', '.docx': '📝', '.txt': '📄', '.md': '📋',
36
+ '.rtf': '📝', '.odt': '📝', '.pages': '📝',
37
+ // Images
38
+ '.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🎞️', '.bmp': '🖼️',
39
+ '.svg': '🎨', '.ico': '🖼️', '.tiff': '🖼️', '.webp': '🖼️',
40
+ // Videos
41
+ '.mp4': '🎬', '.avi': '🎬', '.mov': '🎬', '.wmv': '🎬', '.flv': '🎬',
42
+ '.mkv': '🎬', '.webm': '🎬', '.m4v': '🎬',
43
+ // Audio
44
+ '.mp3': '🎵', '.wav': '🎵', '.flac': '🎵', '.aac': '🎵', '.ogg': '🎵',
45
+ '.wma': '🎵', '.m4a': '🎵',
46
+ // Archives
47
+ '.zip': '🗜️', '.rar': '🗜️', '.7z': '🗜️', '.tar': '🗜️', '.gz': '🗜️',
48
+ '.bz2': '🗜️', '.xz': '🗜️',
49
+ // Code
50
+ '.js': '📜', '.html': '🌐', '.css': '🎨', '.json': '📋', '.xml': '📋',
51
+ '.py': '🐍', '.java': '☕', '.cpp': '⚙️', '.c': '⚙️', '.h': '⚙️',
52
+ '.php': '🐘', '.rb': '💎', '.go': '🐹', '.rs': '🦀', '.swift': '🦉',
53
+ '.kt': '🎯', '.ts': '📘', '.jsx': '⚛️', '.tsx': '⚛️', '.vue': '💚',
54
+ // Config
55
+ '.ini': '⚙️', '.conf': '⚙️', '.cfg': '⚙️', '.yaml': '📋', '.yml': '📋',
56
+ '.toml': '📋', '.env': '🔧',
57
+ // Executables
58
+ '.exe': '⚙️', '.msi': '📦', '.deb': '📦', '.rpm': '📦', '.dmg': '💿',
59
+ '.app': '📱', '.AppImage': '📱',
60
+ // Default
61
+ 'default': '📄'
62
+ };
63
+
64
+ // Toolbar icons
65
+ this.toolbarIcons = [
66
+ { icon: '🏠', action: 'home', tooltip: 'Go Home' },
67
+ { icon: '⬆️', action: 'up', tooltip: 'Parent Directory' },
68
+ { icon: '⬅️', action: 'back', tooltip: 'Back' },
69
+ { icon: '➡️', action: 'forward', tooltip: 'Forward' },
70
+ { icon: '🔄', action: 'refresh', tooltip: 'Refresh' },
71
+ { icon: '📋', action: 'copy', tooltip: 'Copy' },
72
+ { icon: '✂️', action: 'cut', tooltip: 'Cut' },
73
+ { icon: '📄', action: 'paste', tooltip: 'Paste' },
74
+ { icon: '🗑️', action: 'delete', tooltip: 'Delete' },
75
+ { icon: '📁', action: 'newdir', tooltip: 'New Folder' },
76
+ { icon: '📄', action: 'newfile', tooltip: 'New File' },
77
+ { icon: '🔍', action: 'search', tooltip: 'Search' },
78
+ { icon: '👁️', action: 'preview', tooltip: 'Preview' },
79
+ { icon: '📊', action: 'view', tooltip: 'View Mode' },
80
+ { icon: '📖', action: 'bookmark', tooltip: 'Bookmarks' },
81
+ { icon: '🔧', action: 'settings', tooltip: 'Settings' }
82
+ ];
83
+
84
+ // Setup readline interface
85
+ this.rl = readline.createInterface({
86
+ input: process.stdin,
87
+ output: process.stdout
88
+ });
89
+
90
+ // Enable mouse support
91
+ process.stdout.write('\x1b[?1000h'); // Enable mouse tracking
92
+ process.stdout.write('\x1b[?1006h'); // Enable SGR mouse mode
93
+
94
+ // Enable raw mode for immediate key detection
95
+ process.stdin.setRawMode(true);
96
+ process.stdin.resume();
97
+ process.stdin.setEncoding('utf8');
98
+
99
+ // Hide cursor
100
+ process.stdout.write('\x1B[?25l');
101
+
102
+ // Handle cleanup on exit
103
+ process.on('SIGINT', () => this.cleanup());
104
+ process.on('SIGTERM', () => this.cleanup());
105
+ process.on('exit', () => this.cleanup());
106
+ }
107
+
108
+ cleanup() {
109
+ // Show cursor
110
+ process.stdout.write('\x1B[?25h');
111
+ // Disable mouse tracking
112
+ process.stdout.write('\x1b[?1000l');
113
+ process.stdout.write('\x1b[?1006l');
114
+ // Clear screen
115
+ console.clear();
116
+ this.saveBookmarks();
117
+ process.exit(0);
118
+ }
119
+
120
+ loadBookmarks() {
121
+ try {
122
+ const bookmarksPath = path.join(os.homedir(), '.fileman-bookmarks.json');
123
+ if (fs.existsSync(bookmarksPath)) {
124
+ return JSON.parse(fs.readFileSync(bookmarksPath, 'utf8'));
125
+ }
126
+ } catch (error) {
127
+ // Ignore errors, return default bookmarks
128
+ }
129
+ return {
130
+ 'Home': os.homedir(),
131
+ 'Documents': path.join(os.homedir(), 'Documents'),
132
+ 'Downloads': path.join(os.homedir(), 'Downloads'),
133
+ 'Desktop': path.join(os.homedir(), 'Desktop')
134
+ };
135
+ }
136
+
137
+ saveBookmarks() {
138
+ try {
139
+ const bookmarksPath = path.join(os.homedir(), '.fileman-bookmarks.json');
140
+ fs.writeFileSync(bookmarksPath, JSON.stringify(this.bookmarks, null, 2));
141
+ } catch (error) {
142
+ // Ignore save errors
143
+ }
144
+ }
145
+
146
+ getFileIcon(item) {
147
+ if (item.isDirectory) {
148
+ if (item.name.startsWith('.')) return '📂';
149
+ return '📁';
150
+ }
151
+
152
+ const ext = path.extname(item.name).toLowerCase();
153
+ return this.fileIcons[ext] || this.fileIcons.default;
154
+ }
155
+
156
+ async loadDirectory(dirPath = this.currentPath) {
157
+ try {
158
+ const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
159
+
160
+ let items = files
161
+ .filter(item => this.showHidden || !item.name.startsWith('.'))
162
+ .filter(item => !this.filterText || item.name.toLowerCase().includes(this.filterText.toLowerCase()))
163
+ .map(item => {
164
+ const fullPath = path.join(dirPath, item.name);
165
+ let stats;
166
+ try {
167
+ stats = fs.statSync(fullPath);
168
+ } catch (e) {
169
+ stats = { size: 0, mtime: new Date(), mode: 0 };
170
+ }
171
+
172
+ return {
173
+ name: item.name,
174
+ isDirectory: item.isDirectory(),
175
+ isFile: item.isFile(),
176
+ isSymbolicLink: item.isSymbolicLink(),
177
+ size: stats.size,
178
+ mtime: stats.mtime,
179
+ mode: stats.mode,
180
+ fullPath,
181
+ permissions: this.getPermissions(stats.mode),
182
+ type: item.isDirectory() ? 'directory' : path.extname(item.name).toLowerCase()
183
+ };
184
+ });
185
+
186
+ // Sort items
187
+ this.sortItems(items);
188
+
189
+ if (dirPath === this.currentPath) {
190
+ this.items = items;
191
+ // Adjust selected index if necessary
192
+ if (this.selectedIndex >= this.items.length) {
193
+ this.selectedIndex = Math.max(0, this.items.length - 1);
194
+ }
195
+ }
196
+
197
+ return items;
198
+ } catch (error) {
199
+ if (dirPath === this.currentPath) {
200
+ this.items = [];
201
+ this.selectedIndex = 0;
202
+ }
203
+ return [];
204
+ }
205
+ }
206
+
207
+ getPermissions(mode) {
208
+ const perms = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'];
209
+ return [
210
+ perms[(mode >> 6) & 7], // Owner
211
+ perms[(mode >> 3) & 7], // Group
212
+ perms[mode & 7] // Others
213
+ ].join('');
214
+ }
215
+
216
+ sortItems(items) {
217
+ items.sort((a, b) => {
218
+ // Directories first
219
+ if (a.isDirectory && !b.isDirectory) return -1;
220
+ if (!a.isDirectory && b.isDirectory) return 1;
221
+
222
+ let result = 0;
223
+ switch (this.sortBy) {
224
+ case 'size':
225
+ result = a.size - b.size;
226
+ break;
227
+ case 'date':
228
+ result = a.mtime - b.mtime;
229
+ break;
230
+ case 'type':
231
+ result = a.type.localeCompare(b.type);
232
+ break;
233
+ default:
234
+ result = a.name.localeCompare(b.name, undefined, { numeric: true });
235
+ }
236
+
237
+ return this.sortOrder === 'desc' ? -result : result;
238
+ });
239
+ }
240
+
241
+ formatSize(bytes) {
242
+ if (bytes === 0) return '0 B';
243
+ const k = 1024;
244
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
245
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
246
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
247
+ }
248
+
249
+ formatDate(date) {
250
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
251
+ }
252
+
253
+ renderToolbar() {
254
+ const terminalWidth = process.stdout.columns;
255
+ let toolbar = '\x1b[44m\x1b[37m'; // Blue background, white text
256
+
257
+ // Render clickable icons
258
+ for (let i = 0; i < this.toolbarIcons.length; i++) {
259
+ const icon = this.toolbarIcons[i];
260
+ toolbar += ` ${icon.icon} `;
261
+ }
262
+
263
+ // Fill remaining space
264
+ const usedSpace = this.toolbarIcons.length * 3;
265
+ const remainingSpace = Math.max(0, terminalWidth - usedSpace);
266
+ toolbar += ' '.repeat(remainingSpace);
267
+
268
+ toolbar += '\x1b[0m'; // Reset colors
269
+ console.log(toolbar);
270
+ }
271
+
272
+ renderStatusBar() {
273
+ const terminalWidth = process.stdout.columns;
274
+ let statusBar = '\x1b[42m\x1b[30m'; // Green background, black text
275
+
276
+ // Left side - current info
277
+ const selectedItem = this.items[this.selectedIndex];
278
+ let leftInfo = '';
279
+
280
+ if (selectedItem) {
281
+ leftInfo = `${selectedItem.name} | ${selectedItem.isDirectory ? '<DIR>' : this.formatSize(selectedItem.size)}`;
282
+ if (this.selectedItems.size > 0) {
283
+ leftInfo += ` | Selected: ${this.selectedItems.size}`;
284
+ }
285
+ }
286
+
287
+ // Right side - stats
288
+ const rightInfo = `${this.items.length} items | Sort: ${this.sortBy} ${this.sortOrder} | View: ${this.viewMode}`;
289
+
290
+ // Calculate spacing
291
+ const totalInfo = leftInfo.length + rightInfo.length;
292
+ const spacing = Math.max(1, terminalWidth - totalInfo);
293
+
294
+ statusBar += leftInfo + ' '.repeat(spacing) + rightInfo;
295
+
296
+ // Trim if too long
297
+ if (statusBar.length > terminalWidth + 10) { // +10 for color codes
298
+ statusBar = statusBar.substring(0, terminalWidth + 10);
299
+ }
300
+
301
+ statusBar += '\x1b[0m'; // Reset colors
302
+ console.log(statusBar);
303
+ }
304
+
305
+ renderFileList() {
306
+ const terminalHeight = process.stdout.rows - 7; // Account for toolbar, header, status
307
+ const terminalWidth = process.stdout.columns;
308
+
309
+ if (this.splitView) {
310
+ this.renderSplitView(terminalHeight, terminalWidth);
311
+ return;
312
+ }
313
+
314
+ if (this.items.length === 0) {
315
+ console.log('\x1b[2m Empty directory\x1b[0m');
316
+ return;
317
+ }
318
+
319
+ // Calculate start index for scrolling
320
+ const startIndex = Math.max(0, this.selectedIndex - Math.floor(terminalHeight / 2));
321
+ const endIndex = Math.min(this.items.length, startIndex + terminalHeight);
322
+
323
+ for (let i = startIndex; i < endIndex; i++) {
324
+ this.renderFileItem(i, terminalWidth);
325
+ }
326
+
327
+ // Show scroll indicator
328
+ if (this.items.length > terminalHeight) {
329
+ const scrollPercent = Math.round((this.selectedIndex / (this.items.length - 1)) * 100);
330
+ console.log(`\x1b[2m[${scrollPercent}%] ${this.selectedIndex + 1}/${this.items.length}\x1b[0m`);
331
+ }
332
+ }
333
+
334
+ renderFileItem(index, terminalWidth) {
335
+ const item = this.items[index];
336
+ const isSelected = index === this.selectedIndex;
337
+ const isMultiSelected = this.selectedItems.has(index);
338
+
339
+ let prefix = ' ';
340
+ let suffix = '\x1b[0m';
341
+
342
+ if (isSelected && isMultiSelected) {
343
+ prefix = '\x1b[43m\x1b[30m> '; // Yellow background for selected + multi-selected
344
+ } else if (isSelected) {
345
+ prefix = '\x1b[47m\x1b[30m> '; // White background for selected
346
+ } else if (isMultiSelected) {
347
+ prefix = '\x1b[46m\x1b[30m* '; // Cyan background for multi-selected
348
+ }
349
+
350
+ const icon = this.getFileIcon(item);
351
+ let nameColor = this.getFileColor(item);
352
+
353
+ if (this.viewMode === 'simple') {
354
+ const displayName = this.truncateName(item.name, terminalWidth - 10);
355
+ console.log(prefix + icon + ' ' + nameColor + displayName + suffix);
356
+ } else if (this.viewMode === 'detailed') {
357
+ const sizeStr = item.isDirectory ? '<DIR>' : this.formatSize(item.size);
358
+ const dateStr = this.formatDate(item.mtime);
359
+ const permStr = item.permissions;
360
+
361
+ const maxNameLength = Math.max(20, terminalWidth - 45);
362
+ const displayName = this.truncateName(item.name, maxNameLength);
363
+
364
+ console.log(prefix + icon + ' ' + nameColor + displayName.padEnd(maxNameLength) +
365
+ '\x1b[0m ' + permStr + ' ' + sizeStr.padStart(8) + ' ' + dateStr + suffix);
366
+ } else if (this.viewMode === 'grid') {
367
+ // Grid view implementation would go here
368
+ const displayName = this.truncateName(item.name, 15);
369
+ process.stdout.write(prefix + icon + ' ' + nameColor + displayName.padEnd(15) + suffix + ' ');
370
+ if ((index - (Math.max(0, this.selectedIndex - Math.floor((process.stdout.rows - 7) / 2)))) % 4 === 3) {
371
+ console.log('');
372
+ }
373
+ }
374
+ }
375
+
376
+ renderSplitView(terminalHeight, terminalWidth) {
377
+ const halfWidth = Math.floor(terminalWidth / 2) - 1;
378
+
379
+ // Render left pane (current directory)
380
+ console.log('\x1b[1m\x1b[34mLeft Pane: ' + this.currentPath + '\x1b[0m');
381
+ // Left pane file list would go here
382
+
383
+ // Render right pane
384
+ console.log('\x1b[1m\x1b[35mRight Pane: ' + this.rightPanePath + '\x1b[0m');
385
+ // Right pane file list would go here
386
+ }
387
+
388
+ getFileColor(item) {
389
+ if (item.isSymbolicLink) return '\x1b[36m'; // Cyan for symlinks
390
+ if (item.isDirectory) return '\x1b[34m'; // Blue for directories
391
+ if (item.name.startsWith('.')) return '\x1b[2m'; // Dim for hidden files
392
+ if (item.mode & 0o111) return '\x1b[32m'; // Green for executables
393
+
394
+ // Color by file type
395
+ const ext = path.extname(item.name).toLowerCase();
396
+ if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg'].includes(ext)) return '\x1b[35m'; // Magenta for images
397
+ if (['.mp3', '.wav', '.flac', '.aac', '.ogg'].includes(ext)) return '\x1b[33m'; // Yellow for audio
398
+ if (['.mp4', '.avi', '.mov', '.wmv', '.mkv'].includes(ext)) return '\x1b[31m'; // Red for video
399
+ if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) return '\x1b[91m'; // Bright red for archives
400
+
401
+ return '\x1b[37m'; // White for regular files
402
+ }
403
+
404
+ truncateName(name, maxLength) {
405
+ if (name.length <= maxLength) return name;
406
+ return name.substring(0, maxLength - 3) + '...';
407
+ }
408
+
409
+ render() {
410
+ console.clear();
411
+
412
+ const terminalWidth = process.stdout.columns;
413
+
414
+ // Header with path
415
+ console.log('\x1b[1m\x1b[34m' + '='.repeat(terminalWidth) + '\x1b[0m');
416
+ let pathDisplay = this.currentPath;
417
+ if (pathDisplay.length > terminalWidth - 20) {
418
+ pathDisplay = '...' + pathDisplay.substring(pathDisplay.length - (terminalWidth - 23));
419
+ }
420
+ console.log('\x1b[1m\x1b[36mFile Manager\x1b[0m - \x1b[33m' + pathDisplay + '\x1b[0m');
421
+
422
+ // Render toolbar
423
+ this.renderToolbar();
424
+
425
+ // Filter/Search bar
426
+ if (this.searchMode || this.filterText) {
427
+ console.log('\x1b[43m\x1b[30mSearch: ' + this.filterText + '_\x1b[0m');
428
+ }
429
+
430
+ console.log('\x1b[1m\x1b[34m' + '='.repeat(terminalWidth) + '\x1b[0m');
431
+
432
+ // File list
433
+ this.renderFileList();
434
+
435
+ // Preview pane
436
+ if (this.previewMode && this.items.length > 0) {
437
+ this.renderPreview();
438
+ }
439
+
440
+ // Status bar
441
+ this.renderStatusBar();
442
+
443
+ // Show clipboard status
444
+ if (this.clipboard.length > 0) {
445
+ const action = this.clipboardAction === 'copy' ? 'Copied' : 'Cut';
446
+ const items = this.clipboard.length === 1 ?
447
+ path.basename(this.clipboard[0]) :
448
+ `${this.clipboard.length} items`;
449
+ console.log(`\x1b[33m${action}: ${items}\x1b[0m`);
450
+ }
451
+ }
452
+
453
+ renderPreview() {
454
+ const selectedItem = this.items[this.selectedIndex];
455
+ if (!selectedItem) return;
456
+
457
+ console.log('\x1b[1m\x1b[36m--- Preview ---\x1b[0m');
458
+
459
+ if (selectedItem.isDirectory) {
460
+ // Show directory contents
461
+ this.loadDirectory(selectedItem.fullPath).then(items => {
462
+ console.log(`\x1b[2mDirectory contains ${items.length} items\x1b[0m`);
463
+ items.slice(0, 5).forEach(item => {
464
+ console.log(`\x1b[2m ${this.getFileIcon(item)} ${item.name}\x1b[0m`);
465
+ });
466
+ if (items.length > 5) {
467
+ console.log(`\x1b[2m ... and ${items.length - 5} more\x1b[0m`);
468
+ }
469
+ });
470
+ } else {
471
+ // Show file info and content preview
472
+ console.log(`\x1b[2mSize: ${this.formatSize(selectedItem.size)}\x1b[0m`);
473
+ console.log(`\x1b[2mModified: ${this.formatDate(selectedItem.mtime)}\x1b[0m`);
474
+ console.log(`\x1b[2mPermissions: ${selectedItem.permissions}\x1b[0m`);
475
+
476
+ // Try to show file content preview for text files
477
+ const ext = path.extname(selectedItem.name).toLowerCase();
478
+ if (['.txt', '.md', '.js', '.json', '.html', '.css', '.py', '.java'].includes(ext)) {
479
+ try {
480
+ const content = fs.readFileSync(selectedItem.fullPath, 'utf8');
481
+ const lines = content.split('\n').slice(0, 3);
482
+ console.log('\x1b[2mContent preview:\x1b[0m');
483
+ lines.forEach(line => {
484
+ console.log(`\x1b[2m ${line.substring(0, 50)}${line.length > 50 ? '...' : ''}\x1b[0m`);
485
+ });
486
+ } catch (error) {
487
+ console.log('\x1b[2mCannot preview file content\x1b[0m');
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ async handleKeyPress(key) {
494
+ // Handle mouse input
495
+ if (key.startsWith('\x1b[M') || key.startsWith('\x1b[<')) {
496
+ this.handleMouseInput(key);
497
+ return;
498
+ }
499
+
500
+ // Handle search mode
501
+ if (this.searchMode) {
502
+ if (key === '\x1b' || key === '\r' || key === '\n') { // Escape or Enter
503
+ this.searchMode = false;
504
+ if (key === '\r' || key === '\n') {
505
+ await this.loadDirectory();
506
+ }
507
+ } else if (key === '\x7f' || key === '\b') { // Backspace
508
+ this.filterText = this.filterText.slice(0, -1);
509
+ await this.loadDirectory();
510
+ } else if (key.length === 1 && key >= ' ') {
511
+ this.filterText += key;
512
+ await this.loadDirectory();
513
+ }
514
+ return;
515
+ }
516
+
517
+ switch (key) {
518
+ case '\u001b[A': // Up arrow
519
+ case 'k':
520
+ this.moveSelection(-1);
521
+ break;
522
+
523
+ case '\u001b[B': // Down arrow
524
+ case 'j':
525
+ this.moveSelection(1);
526
+ break;
527
+
528
+ case '\r': // Enter
529
+ case '\n':
530
+ await this.openSelected();
531
+ break;
532
+
533
+ case '\u001b[D': // Left arrow
534
+ case 'h':
535
+ await this.goUp();
536
+ break;
537
+
538
+ case '\u001b[C': // Right arrow
539
+ case 'l':
540
+ if (this.items.length > 0 && this.items[this.selectedIndex].isDirectory) {
541
+ await this.openSelected();
542
+ }
543
+ break;
544
+
545
+ case ' ': // Space for multi-select
546
+ this.toggleMultiSelect();
547
+ break;
548
+
549
+ case 'a': // Select all
550
+ this.selectAll();
551
+ break;
552
+
553
+ case 'A': // Deselect all
554
+ this.selectedItems.clear();
555
+ break;
556
+
557
+ case 's': // Sort
558
+ this.cycleSortMode();
559
+ await this.loadDirectory();
560
+ break;
561
+
562
+ case 'S': // Sort order
563
+ this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
564
+ await this.loadDirectory();
565
+ break;
566
+
567
+ case 'v': // View mode
568
+ this.cycleViewMode();
569
+ break;
570
+
571
+ case 'H': // Toggle hidden files
572
+ this.showHidden = !this.showHidden;
573
+ await this.loadDirectory();
574
+ break;
575
+
576
+ case 'c': // Copy
577
+ this.copySelected();
578
+ break;
579
+
580
+ case 'x': // Cut
581
+ this.cutSelected();
582
+ break;
583
+
584
+ case 'p': // Paste
585
+ await this.paste();
586
+ break;
587
+
588
+ case 'd': // Delete
589
+ await this.deleteSelected();
590
+ break;
591
+
592
+ case 'n': // New file
593
+ await this.createNew();
594
+ break;
595
+
596
+ case 'N': // New directory
597
+ await this.createNew('directory');
598
+ break;
599
+
600
+ case 'r': // Rename
601
+ await this.renameSelected();
602
+ break;
603
+
604
+ case 'f': // Search
605
+ case '/':
606
+ this.searchMode = true;
607
+ this.filterText = '';
608
+ break;
609
+
610
+ case 'g': // Go to top
611
+ this.selectedIndex = 0;
612
+ break;
613
+
614
+ case 'G': // Go to bottom
615
+ this.selectedIndex = Math.max(0, this.items.length - 1);
616
+ break;
617
+
618
+ case 'b': // Bookmarks
619
+ await this.showBookmarks();
620
+ break;
621
+
622
+ case 'B': // Add bookmark
623
+ await this.addBookmark();
624
+ break;
625
+
626
+ case 'P': // Toggle preview
627
+ this.previewMode = !this.previewMode;
628
+ break;
629
+
630
+ case 'T': // Toggle split view
631
+ this.splitView = !this.splitView;
632
+ break;
633
+
634
+ case 'R': // Refresh
635
+ await this.loadDirectory();
636
+ break;
637
+
638
+ case 'u': // Go back in history
639
+ this.goBack();
640
+ break;
641
+
642
+ case 'U': // Go forward in history
643
+ this.goForward();
644
+ break;
645
+
646
+ case 'q': // Quit
647
+ case '\u0003': // Ctrl+C
648
+ this.cleanup();
649
+ break;
650
+
651
+ case '?': // Help
652
+ await this.showHelp();
653
+ break;
654
+ }
655
+ }
656
+
657
+ handleMouseInput(sequence) {
658
+ // Parse mouse input for toolbar clicks
659
+ // This is a simplified implementation
660
+ if (sequence.includes('0;')) {
661
+ // Mouse click on first row (toolbar)
662
+ const clickX = parseInt(sequence.match(/;(\d+)/)?.[1] || '0');
663
+ const iconIndex = Math.floor(clickX / 3);
664
+
665
+ if (iconIndex >= 0 && iconIndex < this.toolbarIcons.length) {
666
+ this.executeToolbarAction(this.toolbarIcons[iconIndex].action);
667
+ }
668
+ }
669
+ }
670
+
671
+ async executeToolbarAction(action) {
672
+ switch (action) {
673
+ case 'home':
674
+ this.currentPath = os.homedir();
675
+ this.selectedIndex = 0;
676
+ await this.loadDirectory();
677
+ break;
678
+ case 'up':
679
+ await this.goUp();
680
+ break;
681
+ case 'back':
682
+ this.goBack();
683
+ break;
684
+ case 'forward':
685
+ this.goForward();
686
+ break;
687
+ case 'refresh':
688
+ await this.loadDirectory();
689
+ break;
690
+ case 'copy':
691
+ this.copySelected();
692
+ break;
693
+ case 'cut':
694
+ this.cutSelected();
695
+ break;
696
+ case 'paste':
697
+ await this.paste();
698
+ break;
699
+ case 'delete':
700
+ await this.deleteSelected();
701
+ break;
702
+ case 'newdir':
703
+ await this.createNew('directory');
704
+ break;
705
+ case 'newfile':
706
+ await this.createNew('file');
707
+ break;
708
+ case 'search':
709
+ this.searchMode = true;
710
+ this.filterText = '';
711
+ break;
712
+ case 'preview':
713
+ this.previewMode = !this.previewMode;
714
+ break;
715
+ case 'view':
716
+ this.cycleViewMode();
717
+ break;
718
+ case 'bookmark':
719
+ await this.showBookmarks();
720
+ break;
721
+ case 'settings':
722
+ await this.showSettings();
723
+ break;
724
+ }
725
+ }
726
+
727
+ moveSelection(direction) {
728
+ const newIndex = this.selectedIndex + direction;
729
+ if (newIndex >= 0 && newIndex < this.items.length) {
730
+ this.selectedIndex = newIndex;
731
+ }
732
+ }
733
+
734
+ toggleMultiSelect() {
735
+ if (this.selectedItems.has(this.selectedIndex)) {
736
+ this.selectedItems.delete(this.selectedIndex);
737
+ } else {
738
+ this.selectedItems.add(this.selectedIndex);
739
+ }
740
+ this.multiSelectMode = this.selectedItems.size > 0;
741
+ }
742
+
743
+ selectAll() {
744
+ for (let i = 0; i < this.items.length; i++) {
745
+ this.selectedItems.add(i);
746
+ }
747
+ this.multiSelectMode = true;
748
+ }
749
+
750
+ cycleSortMode() {
751
+ const modes = ['name', 'size', 'date', 'type'];
752
+ const currentIndex = modes.indexOf(this.sortBy);
753
+ this.sortBy = modes[(currentIndex + 1) % modes.length];
754
+ }
755
+
756
+ cycleViewMode() {
757
+ const modes = ['detailed', 'simple', 'grid'];
758
+ const currentIndex = modes.indexOf(this.viewMode);
759
+ this.viewMode = modes[(currentIndex + 1) % modes.length];
760
+ }
761
+
762
+ async openSelected() {
763
+ if (this.items.length === 0) return;
764
+
765
+ const selected = this.items[this.selectedIndex];
766
+
767
+ if (selected.isDirectory) {
768
+ this.addToHistory(this.currentPath);
769
+ this.currentPath = selected.fullPath;
770
+ this.selectedIndex = 0;
771
+ this.selectedItems.clear();
772
+ await this.loadDirectory();
773
+ } else {
774
+ // Try to open file with system default application
775
+ try {
776
+ const command = process.platform === 'win32' ? 'start' :
777
+ process.platform === 'darwin' ? 'open' : 'xdg-open';
778
+ execSync(`${command} "${selected.fullPath}"`, { stdio: 'ignore' });
779
+ } catch (error) {
780
+ console.log(`\x1b[31mCannot open file: ${selected.name}\x1b[0m`);
781
+ await this.waitForKey();
782
+ }
783
+ }
784
+ }
785
+
786
+ async goUp() {
787
+ const parentPath = path.dirname(this.currentPath);
788
+ if (parentPath !== this.currentPath) {
789
+ this.addToHistory(this.currentPath);
790
+ this.currentPath = parentPath;
791
+ this.selectedIndex = 0;
792
+ this.selectedItems.clear();
793
+ await this.loadDirectory();
794
+ }
795
+ }
796
+
797
+ addToHistory(path) {
798
+ // Remove forward history when adding new path
799
+ this.history = this.history.slice(0, this.historyIndex + 1);
800
+ this.history.push(path);
801
+ this.historyIndex = this.history.length - 1;
802
+
803
+ // Limit history size
804
+ if (this.history.length > 50) {
805
+ this.history.shift();
806
+ this.historyIndex--;
807
+ }
808
+ }
809
+
810
+ goBack() {
811
+ if (this.historyIndex > 0) {
812
+ this.historyIndex--;
813
+ this.currentPath = this.history[this.historyIndex];
814
+ this.selectedIndex = 0;
815
+ this.selectedItems.clear();
816
+ this.loadDirectory();
817
+ }
818
+ }
819
+
820
+ goForward() {
821
+ if (this.historyIndex < this.history.length - 1) {
822
+ this.historyIndex++;
823
+ this.currentPath = this.history[this.historyIndex];
824
+ this.selectedIndex = 0;
825
+ this.selectedItems.clear();
826
+ this.loadDirectory();
827
+ }
828
+ }
829
+
830
+ copySelected() {
831
+ if (this.selectedItems.size > 0) {
832
+ this.clipboard = Array.from(this.selectedItems).map(i => this.items[i].fullPath);
833
+ } else if (this.items.length > 0) {
834
+ this.clipboard = [this.items[this.selectedIndex].fullPath];
835
+ }
836
+ this.clipboardAction = 'copy';
837
+ }
838
+
839
+ cutSelected() {
840
+ if (this.selectedItems.size > 0) {
841
+ this.clipboard = Array.from(this.selectedItems).map(i => this.items[i].fullPath);
842
+ } else if (this.items.length > 0) {
843
+ this.clipboard = [this.items[this.selectedIndex].fullPath];
844
+ }
845
+ this.clipboardAction = 'cut';
846
+ }
847
+
848
+ async paste() {
849
+ if (this.clipboard.length === 0) return;
850
+
851
+ try {
852
+ for (const sourcePath of this.clipboard) {
853
+ const fileName = path.basename(sourcePath);
854
+ const destPath = path.join(this.currentPath, fileName);
855
+
856
+ if (this.clipboardAction === 'copy') {
857
+ await this.copyFile(sourcePath, destPath);
858
+ } else {
859
+ await fs.promises.rename(sourcePath, destPath);
860
+ }
861
+ }
862
+
863
+ if (this.clipboardAction === 'cut') {
864
+ this.clipboard = [];
865
+ this.clipboardAction = null;
866
+ }
867
+
868
+ this.selectedItems.clear();
869
+ await this.loadDirectory();
870
+ } catch (error) {
871
+ console.log(`\x1b[31mPaste failed: ${error.message}\x1b[0m`);
872
+ await this.waitForKey();
873
+ }
874
+ }
875
+
876
+ async copyFile(src, dest) {
877
+ const stat = await fs.promises.stat(src);
878
+
879
+ if (stat.isDirectory()) {
880
+ await fs.promises.mkdir(dest, { recursive: true });
881
+ const files = await fs.promises.readdir(src);
882
+
883
+ for (const file of files) {
884
+ await this.copyFile(path.join(src, file), path.join(dest, file));
885
+ }
886
+ } else {
887
+ await fs.promises.copyFile(src, dest);
888
+ }
889
+ }
890
+
891
+ async deleteSelected() {
892
+ const itemsToDelete = this.selectedItems.size > 0 ?
893
+ Array.from(this.selectedItems).map(i => this.items[i]) :
894
+ this.items.length > 0 ? [this.items[this.selectedIndex]] : [];
895
+
896
+ if (itemsToDelete.length === 0) return;
897
+
898
+ const names = itemsToDelete.map(item => item.name).join(', ');
899
+ console.log(`\x1b[31mDelete ${itemsToDelete.length} item(s): ${names}? (y/N)\x1b[0m`);
900
+
901
+ const confirmation = await this.getInput();
902
+ if (confirmation.toLowerCase() === 'y') {
903
+ try {
904
+ for (const item of itemsToDelete) {
905
+ if (item.isDirectory) {
906
+ await fs.promises.rmdir(item.fullPath, { recursive: true });
907
+ } else {
908
+ await fs.promises.unlink(item.fullPath);
909
+ }
910
+ }
911
+
912
+ this.selectedItems.clear();
913
+ if (this.selectedIndex >= this.items.length - itemsToDelete.length) {
914
+ this.selectedIndex = Math.max(0, this.selectedIndex - itemsToDelete.length);
915
+ }
916
+
917
+ await this.loadDirectory();
918
+ } catch (error) {
919
+ console.log(`\x1b[31mDelete failed: ${error.message}\x1b[0m`);
920
+ await this.waitForKey();
921
+ }
922
+ }
923
+ }
924
+
925
+ async renameSelected() {
926
+ if (this.items.length === 0) return;
927
+
928
+ const selected = this.items[this.selectedIndex];
929
+ console.log(`\x1b[33mRename "${selected.name}" to:\x1b[0m`);
930
+
931
+ const newName = await this.getInput();
932
+ if (newName.trim() && newName.trim() !== selected.name) {
933
+ try {
934
+ const newPath = path.join(this.currentPath, newName.trim());
935
+ await fs.promises.rename(selected.fullPath, newPath);
936
+ await this.loadDirectory();
937
+ } catch (error) {
938
+ console.log(`\x1b[31mRename failed: ${error.message}\x1b[0m`);
939
+ await this.waitForKey();
940
+ }
941
+ }
942
+ }
943
+
944
+ async createNew(type = null) {
945
+ if (!type) {
946
+ console.log('\x1b[33mCreate (f)ile or (d)irectory? (f/d)\x1b[0m');
947
+ const typeInput = await this.getInput();
948
+ type = typeInput.toLowerCase() === 'd' ? 'directory' : 'file';
949
+ }
950
+
951
+ const itemType = type === 'directory' ? 'directory' : 'file';
952
+ console.log(`\x1b[33mEnter ${itemType} name:\x1b[0m`);
953
+ const name = await this.getInput();
954
+
955
+ if (name.trim()) {
956
+ try {
957
+ const fullPath = path.join(this.currentPath, name.trim());
958
+
959
+ if (type === 'directory') {
960
+ await fs.promises.mkdir(fullPath);
961
+ } else {
962
+ await fs.promises.writeFile(fullPath, '');
963
+ }
964
+
965
+ await this.loadDirectory();
966
+ } catch (error) {
967
+ console.log(`\x1b[31mCreate failed: ${error.message}\x1b[0m`);
968
+ await this.waitForKey();
969
+ }
970
+ }
971
+ }
972
+
973
+ async showBookmarks() {
974
+ console.clear();
975
+ console.log('\x1b[1m\x1b[36m=== Bookmarks ===\x1b[0m');
976
+
977
+ const bookmarkNames = Object.keys(this.bookmarks);
978
+ for (let i = 0; i < bookmarkNames.length; i++) {
979
+ const name = bookmarkNames[i];
980
+ const path = this.bookmarks[name];
981
+ console.log(`${i + 1}. ${name} -> ${path}`);
982
+ }
983
+
984
+ console.log('\nEnter bookmark number to navigate, or press Enter to cancel:');
985
+ const input = await this.getInput();
986
+ const num = parseInt(input);
987
+
988
+ if (num > 0 && num <= bookmarkNames.length) {
989
+ const bookmarkName = bookmarkNames[num - 1];
990
+ const bookmarkPath = this.bookmarks[bookmarkName];
991
+
992
+ if (fs.existsSync(bookmarkPath)) {
993
+ this.addToHistory(this.currentPath);
994
+ this.currentPath = bookmarkPath;
995
+ this.selectedIndex = 0;
996
+ this.selectedItems.clear();
997
+ await this.loadDirectory();
998
+ } else {
999
+ console.log(`\x1b[31mBookmark path does not exist: ${bookmarkPath}\x1b[0m`);
1000
+ await this.waitForKey();
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ async addBookmark() {
1006
+ console.log(`\x1b[33mAdd bookmark for "${this.currentPath}".\nEnter bookmark name:\x1b[0m`);
1007
+ const name = await this.getInput();
1008
+
1009
+ if (name.trim()) {
1010
+ this.bookmarks[name.trim()] = this.currentPath;
1011
+ this.saveBookmarks();
1012
+ console.log(`\x1b[32mBookmark "${name.trim()}" added!\x1b[0m`);
1013
+ await this.waitForKey();
1014
+ }
1015
+ }
1016
+
1017
+ async showSettings() {
1018
+ console.clear();
1019
+ console.log('\x1b[1m\x1b[36m=== Settings ===\x1b[0m');
1020
+ console.log(`1. Show hidden files: ${this.showHidden ? 'ON' : 'OFF'}`);
1021
+ console.log(`2. Sort by: ${this.sortBy}`);
1022
+ console.log(`3. Sort order: ${this.sortOrder}`);
1023
+ console.log(`4. View mode: ${this.viewMode}`);
1024
+ console.log(`5. Preview mode: ${this.previewMode ? 'ON' : 'OFF'}`);
1025
+ console.log(`6. Split view: ${this.splitView ? 'ON' : 'OFF'}`);
1026
+ console.log('\nEnter setting number to toggle, or press Enter to cancel:');
1027
+
1028
+ const input = await this.getInput();
1029
+ const num = parseInt(input);
1030
+
1031
+ switch (num) {
1032
+ case 1:
1033
+ this.showHidden = !this.showHidden;
1034
+ await this.loadDirectory();
1035
+ break;
1036
+ case 2:
1037
+ this.cycleSortMode();
1038
+ await this.loadDirectory();
1039
+ break;
1040
+ case 3:
1041
+ this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
1042
+ await this.loadDirectory();
1043
+ break;
1044
+ case 4:
1045
+ this.cycleViewMode();
1046
+ break;
1047
+ case 5:
1048
+ this.previewMode = !this.previewMode;
1049
+ break;
1050
+ case 6:
1051
+ this.splitView = !this.splitView;
1052
+ break;
1053
+ }
1054
+ }
1055
+
1056
+ async showHelp() {
1057
+ console.clear();
1058
+ console.log('\x1b[1m\x1b[36m=== File Manager Help ===\x1b[0m');
1059
+ console.log('\x1b[1mNavigation:\x1b[0m');
1060
+ console.log(' ↑/↓, j/k - Move up/down');
1061
+ console.log(' ←/→, h/l - Parent dir/Enter dir');
1062
+ console.log(' Enter - Open file/directory');
1063
+ console.log(' g/G - Go to top/bottom');
1064
+ console.log(' u/U - Back/Forward in history');
1065
+ console.log('');
1066
+ console.log('\x1b[1mFile Operations:\x1b[0m');
1067
+ console.log(' Space - Multi-select toggle');
1068
+ console.log(' a/A - Select all/Deselect all');
1069
+ console.log(' c/x/p - Copy/Cut/Paste');
1070
+ console.log(' d - Delete');
1071
+ console.log(' r - Rename');
1072
+ console.log(' n/N - New file/New directory');
1073
+ console.log('');
1074
+ console.log('\x1b[1mView & Search:\x1b[0m');
1075
+ console.log(' v - Cycle view mode');
1076
+ console.log(' s/S - Sort by/Sort order');
1077
+ console.log(' H - Toggle hidden files');
1078
+ console.log(' f,/ - Search/Filter');
1079
+ console.log(' P - Toggle preview');
1080
+ console.log(' T - Toggle split view');
1081
+ console.log(' R - Refresh');
1082
+ console.log('');
1083
+ console.log('\x1b[1mBookmarks & Other:\x1b[0m');
1084
+ console.log(' b/B - Show bookmarks/Add bookmark');
1085
+ console.log(' ? - Show this help');
1086
+ console.log(' q - Quit');
1087
+ console.log('');
1088
+ console.log('\x1b[1mToolbar:\x1b[0m');
1089
+ console.log(' Click on toolbar icons for quick actions');
1090
+ console.log('');
1091
+ console.log('Press any key to continue...');
1092
+ await this.waitForKey();
1093
+ }
1094
+
1095
+ async getInput() {
1096
+ // Show cursor temporarily
1097
+ process.stdout.write('\x1B[?25h');
1098
+
1099
+ return new Promise((resolve) => {
1100
+ this.rl.question('', (answer) => {
1101
+ // Hide cursor again
1102
+ process.stdout.write('\x1B[?25l');
1103
+ resolve(answer);
1104
+ });
1105
+ });
1106
+ }
1107
+
1108
+ async waitForKey() {
1109
+ return new Promise((resolve) => {
1110
+ const handler = () => {
1111
+ process.stdin.removeListener('data', handler);
1112
+ resolve();
1113
+ };
1114
+ process.stdin.once('data', handler);
1115
+ });
1116
+ }
1117
+
1118
+ async run() {
1119
+ await this.loadDirectory();
1120
+ this.render();
1121
+
1122
+ process.stdin.on('data', async (key) => {
1123
+ await this.handleKeyPress(key);
1124
+ this.render();
1125
+ });
1126
+ }
1127
+ }
1128
+
1129
+ var fm = new FileManager()
1130
+ fm.run()