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/docs/assets/js/c3a618e1.965a31da.js +1 -1
- package/docs/functions/index.html +2 -2
- package/docs-config/src/functions/index.md +1 -1
- package/package.json +3 -2
- package/readme.md +18 -30
- package/src/fm.js +1130 -0
- package/src/github-api.js +15 -15
- package/bun.lock +0 -159
- package/docs-config/bun.lock +0 -6139
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()
|