class FileManagerModule {
constructor() {
this.commands = {};
this.commandsMeta = {
'files': { argType: 'file' }, 'preview': { argType: 'file' },
};
this.terminal = null;
this.currentPath = [];
this._overlay = null;
this._selectedFile = null;
this._searchFilter = '';
this._rawMode = false;
this._inlineHtml = false;
}
requirements() {
return { met: true, missing: [] };
}
getHelpInfo() {
return {
'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip',
'preview': 'preview — Preview a specific file',
};
}
registerCommands() {
this.commands['files'] = () => this._openOverlay();
this.commands['preview'] = (args) => this._previewCommand(args);
}
_previewCommand(args) {
if (!args[0]) return this.terminal.print('Usage: preview ', 'warning');
const path = args.join(' ');
this._openOverlay();
this._navigateToPathFromParts(path);
const content = this.terminal.fs.readFile(path);
if (content === null || content === false) {
this._renderPreviewError('File not found: ' + path);
return;
}
this._selectedFile = path;
this._renderPreview(path, content);
this._highlightSelected();
}
_navigateToPathFromParts(path) {
const parts = path.split('/').filter(p => p);
if (parts.length <= 1) {
this.currentPath = [];
return;
}
this.currentPath = parts.slice(0, -1);
}
_detectType(filePath) {
const ext = filePath.split('.').pop().toLowerCase();
if (!ext || ext === filePath) return 'other';
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'];
const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];
const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
const codeExts = ['js', 'py', 'css', 'yaml', 'yml', 'toml'];
if (ext === 'md') return 'md';
if (imageExts.includes(ext)) return 'image';
if (videoExts.includes(ext)) return 'video';
if (audioExts.includes(ext)) return 'audio';
if (codeExts.includes(ext)) return 'code';
if (ext === 'html') return 'html';
if (ext === 'json') return 'json';
if (ext === 'csv') return 'csv';
if (ext === 'pdf') return 'pdf';
if (ext === 'svg') return 'svg';
if (ext === 'txt') return 'text';
return 'other';
}
_renderMarkdown(text) {
if (!text) return '';
try {
let html = text
.replace(/&/g, '&')
.replace(//g, '>');
// code blocks first
html = html.replace(/```([\s\S]*?)```/g, '$1
');
// inline code
html = html.replace(/`([^`]+)`/g, '$1');
// headers
html = html.replace(/^### (.+)$/gm, '$1
');
html = html.replace(/^## (.+)$/gm, '$1
');
html = html.replace(/^# (.+)$/gm, '$1
');
// bold
html = html.replace(/\*\*(.+?)\*\*/g, '$1');
// italic
html = html.replace(/\*(.+?)\*/g, '$1');
// links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
// horizontal rules
html = html.replace(/^---$/gm, '
');
// unordered lists
html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '$1');
// paragraphs - double newlines
html = html.replace(/\n\n/g, '
');
html = '
' + html + '
';
return html;
} catch (e) {
return '' + text.replace(/&/g, '&').replace(//g, '>') + '
';
}
}
_getPreviewHtml(path, content) {
if (content === null || content === false) {
return 'File not found: ' + this._escapeHtml(path) + '
';
}
const type = this._detectType(path);
const ext = path.split('.').pop().toLowerCase();
const fileName = path.split('/').pop() || path;
// Handle binary detection for media types
if (typeof content !== 'string') {
return 'Cannot preview: non-string content
';
}
const isLikelyBinary = content.length > 0 && !content.startsWith('\n') && !content.startsWith('\r') && !content.startsWith(' ') && !content.startsWith('<') && !content.startsWith('{') && !content.startsWith('[') && content.charCodeAt(0) < 32 && content.charCodeAt(0) !== 9 && content.charCodeAt(0) !== 10 && content.charCodeAt(0) !== 13;
const isTextContent = typeof content === 'string' && (content.startsWith('{') || content.startsWith('[') || content.startsWith('<') || content.startsWith('#') || /^[\w\s\n\r\t.,;:!?()\-=+*@#$%^&"'\/\\|`~]/.test(content.substring(0, 100)));
if (type === 'md' && isTextContent) {
return '' + this._renderMarkdown(content) + '
';
}
if (type === 'html') {
// Render HTML in an inline sandbox frame
const safeHtml = content
.replace(/