class FileManagerModule {
constructor() {
this.commands = {};
this.terminal = null;
this.currentPath = [];
this._overlay = null;
this._selectedFile = null;
this._searchFilter = '';
this._rawMode = false;
}
requirements() {
return { met: true, missing: [] };
}
getHelpInfo() {
return {
'files': 'Open the file manager overlay with VFS directory listing and preview',
'preview': 'Preview a specific file (usage: preview )',
};
}
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'];
const videoExts = ['mp4', 'webm'];
const audioExts = ['mp3', 'wav', 'ogg'];
const codeExts = ['js', 'py', 'html', '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 === '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
const isLikelyBinary = typeof content === 'string' && 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 === 'text') {
const truncated = content.substring(0, 2000);
const extra = content.length > 2000 ? '(truncated to 2000 chars)
' : '';
return '' + this._escapeHtml(truncated) + extra + '
';
}
if (type === 'json') {
try {
const parsed = JSON.parse(content);
const formatted = JSON.stringify(parsed, null, 2);
const html = this._syntaxHighlightJson(formatted);
return '' + html + '
';
} catch (e) {
return '' + this._escapeHtml(content.substring(0, 5000)) + '
';
}
}
if (type === 'code') {
const lines = content.split('\n');
const totalLines = lines.length;
const maxLines = 150;
const displayLines = lines.slice(0, maxLines);
let html = '';
displayLines.forEach((line, i) => {
const lineNum = i + 1;
const escaped = this._escapeHtml(line);
html += '| ' + lineNum + ' | ';
html += '' + (escaped || ' ') + ' |
';
});
html += '
';
if (totalLines > maxLines) {
html += '(showing ' + maxLines + ' of ' + totalLines + ' lines)
';
}
return html;
}
if (type === 'image') {
if (isLikelyBinary && !isTextContent) {
return '📷 Image — download to view
';
}
const base64 = btoa(unescape(encodeURIComponent(content)));
return '';
}
if (type === 'svg') {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const scripts = doc.querySelectorAll('script');
scripts.forEach(s => s.remove());
// Remove event handlers from svg elements
const allEls = doc.querySelectorAll('*');
allEls.forEach(el => {
for (let i = el.attributes.length - 1; i >= 0; i--) {
const attr = el.attributes[i].name;
if (attr.startsWith('on')) el.removeAttribute(attr);
}
});
const cleanSvg = doc.body.innerHTML;
return '' + cleanSvg + '
';
} catch (e) {
return '' + this._escapeHtml(content.substring(0, 5000)) + '
';
}
}
if (type === 'video') {
if (isLikelyBinary && !isTextContent) {
return '📷 Video — download to view
';
}
const base64 = btoa(unescape(encodeURIComponent(content)));
return '';
}
if (type === 'audio') {
if (isLikelyBinary && !isTextContent) {
return '🎵 Audio — download to play
';
}
const base64 = btoa(unescape(encodeURIComponent(content)));
return '';
}
if (type === 'pdf') {
try {
const base64 = btoa(unescape(encodeURIComponent(content)));
return '';
} catch (e) {
return '📄 PDF — download to view
';
}
}
if (type === 'csv') {
try {
const lines = content.split('\n').filter(l => l.trim());
const maxRows = 50;
const rows = lines.slice(0, maxRows).map(l => l.split(',').map(c => c.trim().replace(/^"|"$/g, '')));
let html = '';
if (rows.length > 0) {
html += '';
rows[0].forEach(cell => {
html += '| ' + this._escapeHtml(cell) + ' | ';
});
html += '
';
rows.slice(1).forEach(row => {
html += '';
row.forEach(cell => {
html += '| ' + this._escapeHtml(cell) + ' | ';
});
html += '
';
});
html += '
';
if (lines.length > maxRows) {
html += '(showing ' + maxRows + ' of ' + lines.length + ' rows)
';
}
}
return html;
} catch (e) {
return '' + this._escapeHtml(content.substring(0, 5000)) + '
';
}
}
// 'other' — raw text fallback
if (typeof content === 'string' && isTextContent) {
const lines = content.split('\n');
const maxLines = 80;
const displayLines = lines.slice(0, maxLines);
const truncated = lines.length > maxLines;
let html = '';
displayLines.forEach((line, i) => {
html += this._escapeHtml(line) + '\n';
});
html += '';
if (truncated) {
html += '(showing ' + maxLines + ' of ' + lines.length + ' lines)
';
}
return html;
}
return '📁 Preview not available for this file type
';
}
_syntaxHighlightJson(jsonStr) {
return jsonStr.replace(/("(?:\\.|[^"\\])*")\s*:/g, '$1:')
.replace(/:\s*("(?:\\.|[^"\\])*")/g, ': $1')
.replace(/:\s*(\d+\.?\d*)/g, ': $1')
.replace(/:\s*(true|false|null)/g, ': $1');
}
_escapeHtml(str) {
if (typeof str !== 'string') return '';
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
_formatSize(size) {
if (!size && size !== 0) return '';
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
return (size / (1024 * 1024)).toFixed(1) + ' MB';
}
_createOverlay() {
if (this._overlay) return;
const overlay = document.createElement('div');
overlay.id = 'filemgr-overlay';
overlay.style.cssText = 'position:fixed;inset:0;z-index:1800;display:flex;flex-direction:column;background:var(--bg-color);color:var(--text-color);font-family:var(--font-primary);font-size:14px;';
// Inject responsive styles for the overlay
const styleId = 'filemgr-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
#filemgr-overlay.responsive .filemgr-main {
flex-direction: column !important;
}
#filemgr-overlay.responsive #filemgr-sidebar {
width: 100% !important;
max-height: 40vh;
}
#filemgr-overlay.responsive #filemgr-preview {
width: 100% !important;
}
#filemgr-overlay .filemgr-main {
display: flex;
flex: 1;
min-height: 0;
}
@media (max-width: 600px) {
#filemgr-overlay .filemgr-main {
flex-direction: column !important;
}
#filemgr-overlay #filemgr-sidebar {
width: 100% !important;
max-height: 40vh;
border-right: none !important;
border-bottom: 1px solid var(--border-color);
}
#filemgr-overlay #filemgr-preview {
width: 100% !important;
min-height: 40vh;
}
#filemgr-action-bar {
flex-wrap: wrap;
gap: 6px;
}
#filemgr-action-bar button {
min-width: 44px;
min-height: 44px;
font-size: 0.9em;
}
}
`;
document.head.appendChild(style);
}
// Toolbar
const toolbar = document.createElement('div');
toolbar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
const closeBtn = document.createElement('button');
closeBtn.textContent = '✕';
closeBtn.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;font-family:inherit;font-size:1em;min-height:44px;touch-action:manipulation;';
closeBtn.onclick = () => this._closeOverlay();
toolbar.appendChild(closeBtn);
const titleSpan = document.createElement('span');
titleSpan.textContent = '📁 File Manager';
titleSpan.style.cssText = 'font-weight:bold;margin-right:8px;';
toolbar.appendChild(titleSpan);
// Breadcrumbs
const breadcrumbs = document.createElement('div');
breadcrumbs.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;font-size:0.9em;';
breadcrumbs.id = 'filemgr-breadcrumbs';
// Root breadcrumb
const rootSeg = document.createElement('span');
rootSeg.textContent = '/';
rootSeg.style.cssText = 'color:var(--accent-color);cursor:pointer;padding:2px 4px;border-radius:3px;';
rootSeg.onmouseenter = () => { rootSeg.style.background = 'var(--bg-tertiary)'; };
rootSeg.onmouseleave = () => { rootSeg.style.background = 'transparent'; };
rootSeg.onclick = () => this._navigateTo([]);
breadcrumbs.appendChild(rootSeg);
toolbar.appendChild(breadcrumbs);
// Search input
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = '🔍 Filter files...';
searchInput.style.cssText = 'flex:1;min-width:120px;max-width:300px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);font-family:var(--font-primary);font-size:0.9em;outline:none;';
searchInput.oninput = (e) => {
this._searchFilter = e.target.value.toLowerCase();
this._refreshFileList();
};
toolbar.appendChild(searchInput);
overlay.appendChild(toolbar);
// Main area — wraps sidebar + preview in a responsive flex container
const mainArea = document.createElement('div');
mainArea.id = 'filemgr-main-container';
mainArea.className = 'filemgr-main';
mainArea.style.cssText = 'overflow:hidden;';
// Sidebar
const sidebar = document.createElement('div');
sidebar.id = 'filemgr-sidebar';
sidebar.style.cssText = 'width:30%;min-width:200px;overflow-y:auto;background:var(--bg-secondary);border-right:1px solid var(--border-color);';
mainArea.appendChild(sidebar);
// Preview pane
const preview = document.createElement('div');
preview.id = 'filemgr-preview';
preview.style.cssText = 'flex:1;overflow-y:auto;padding:0;';
mainArea.appendChild(preview);
overlay.appendChild(mainArea);
// Action bar
const actionBar = document.createElement('div');
actionBar.id = 'filemgr-action-bar';
actionBar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
const statusSpan = document.createElement('span');
statusSpan.id = 'filemgr-status';
statusSpan.style.cssText = 'flex:1;color:var(--text-muted);font-size:0.85em;';
statusSpan.textContent = 'No file selected';
actionBar.appendChild(statusSpan);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑 Delete';
deleteBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--error-color);color:var(--error-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
deleteBtn.onclick = () => this._actionDelete();
actionBar.appendChild(deleteBtn);
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '⬇ Download';
downloadBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
downloadBtn.onclick = () => this._actionDownload();
actionBar.appendChild(downloadBtn);
const editBtn = document.createElement('button');
editBtn.textContent = '✏ Edit';
editBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--accent-color);color:var(--accent-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
editBtn.onclick = () => this._actionEdit();
actionBar.appendChild(editBtn);
const previewToggleBtn = document.createElement('button');
previewToggleBtn.id = 'filemgr-toggle-raw';
previewToggleBtn.textContent = '📄 Raw';
previewToggleBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-muted);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
previewToggleBtn.onclick = () => this._togglePreviewMode();
actionBar.appendChild(previewToggleBtn);
overlay.appendChild(actionBar);
this._overlay = overlay;
return overlay;
}
_openOverlay() {
if (this._overlay) {
this._overlay.style.display = 'flex';
this._refreshFileList();
return;
}
const overlay = this._createOverlay();
document.body.appendChild(overlay);
this._navigateTo(this.currentPath);
}
_closeOverlay() {
if (this._overlay) {
this._overlay.style.display = 'none';
}
}
_clearOverlay() {
if (this._overlay && this._overlay.parentNode) {
this._overlay.parentNode.removeChild(this._overlay);
}
this._overlay = null;
this._selectedFile = null;
this._searchFilter = '';
this._rawMode = false;
}
_navigateTo(pathParts) {
this.currentPath = pathParts || [];
this._selectedFile = null;
this._searchFilter = '';
const searchInput = this._overlay?.querySelector('input[type="text"]');
if (searchInput) searchInput.value = '';
this._updateBreadcrumbs();
this._refreshFileList();
this._clearPreview();
this._updateStatus('No file selected');
}
_updateBreadcrumbs() {
const breadcrumbs = document.getElementById('filemgr-breadcrumbs');
if (!breadcrumbs) return;
// Keep root segment, remove rest
while (breadcrumbs.children.length > 1) {
breadcrumbs.removeChild(breadcrumbs.lastChild);
}
let accumulated = '';
this.currentPath.forEach((part, i) => {
const sep = document.createElement('span');
sep.textContent = ' › ';
sep.style.cssText = 'color:var(--text-muted);margin:0 2px;';
breadcrumbs.appendChild(sep);
const seg = document.createElement('span');
seg.textContent = part;
seg.style.cssText = 'cursor:pointer;padding:2px 4px;border-radius:3px;color:var(--text-color);';
if (i === this.currentPath.length - 1) {
seg.style.color = 'var(--accent-color)';
seg.style.fontWeight = 'bold';
}
seg.onmouseenter = () => { seg.style.background = 'var(--bg-tertiary)'; };
seg.onmouseleave = () => { seg.style.background = 'transparent'; };
const pathUpTo = this.currentPath.slice(0, i + 1);
seg.onclick = () => this._navigateTo(pathUpTo);
breadcrumbs.appendChild(seg);
});
}
_refreshFileList() {
const sidebar = document.getElementById('filemgr-sidebar');
if (!sidebar) return;
// Read directory
const pathStr = '/' + this.currentPath.join('/');
const entries = this.terminal.fs.ls(pathStr) || [];
// Separate dirs and files
const dirs = entries.filter(e => e.type === 'dir');
const files = entries.filter(e => e.type !== 'dir');
// Sort alphabetically
dirs.sort((a, b) => a.name.localeCompare(b.name));
files.sort((a, b) => a.name.localeCompare(b.name));
// Apply search filter
let filteredDirs = dirs;
let filteredFiles = files;
if (this._searchFilter) {
const q = this._searchFilter;
filteredDirs = dirs.filter(e => e.name.toLowerCase().includes(q));
filteredFiles = files.filter(e => e.name.toLowerCase().includes(q));
}
sidebar.innerHTML = '';
if (filteredDirs.length === 0 && filteredFiles.length === 0) {
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = 'padding:20px;text-align:center;color:var(--text-muted);';
emptyMsg.textContent = this._searchFilter ? 'No matching files' : '(empty)';
sidebar.appendChild(emptyMsg);
this._updateStatus('0 files');
return;
}
// Add directory items
filteredDirs.forEach(entry => {
const item = document.createElement('div');
const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
item.dataset.path = fullPath;
item.dataset.type = 'dir';
item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;';
item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
item.onclick = () => {
const newPath = [...this.currentPath, entry.name];
this._navigateTo(newPath);
};
const icon = document.createElement('span');
icon.textContent = '📁';
item.appendChild(icon);
const nameSpan = document.createElement('span');
nameSpan.textContent = entry.name + '/';
nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
item.appendChild(nameSpan);
sidebar.appendChild(item);
});
// Add file items
filteredFiles.forEach(entry => {
const item = document.createElement('div');
const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
const isSelected = this._selectedFile === fullPath;
item.dataset.path = fullPath;
item.dataset.type = 'file';
item.dataset.selected = isSelected ? 'true' : 'false';
const bgColor = isSelected ? 'var(--bg-tertiary)' : 'transparent';
item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;background:' + bgColor + ';';
if (isSelected) {
item.style.borderLeft = '3px solid var(--accent-color)';
}
item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
item.onclick = () => {
this._selectedFile = fullPath;
this._highlightSelected();
const content = this.terminal.fs.readFile(fullPath);
this._renderPreview(fullPath, content);
};
const icon = document.createElement('span');
icon.textContent = '📄';
item.appendChild(icon);
const nameSpan = document.createElement('span');
nameSpan.textContent = entry.name;
nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
item.appendChild(nameSpan);
const sizeSpan = document.createElement('span');
sizeSpan.textContent = this._formatSize(entry.size);
sizeSpan.style.cssText = 'color:var(--text-muted);font-size:0.8em;white-space:nowrap;margin-left:4px;';
item.appendChild(sizeSpan);
sidebar.appendChild(item);
});
const totalCount = entries.length;
this._updateStatus(totalCount + ' item' + (totalCount !== 1 ? 's' : ''));
}
_highlightSelected() {
const sidebar = document.getElementById('filemgr-sidebar');
if (!sidebar) return;
const items = sidebar.querySelectorAll('[data-type="file"]');
items.forEach(item => {
const selected = item.dataset.path === this._selectedFile;
item.dataset.selected = selected ? 'true' : 'false';
item.style.background = selected ? 'var(--bg-tertiary)' : 'transparent';
item.style.borderLeft = selected ? '3px solid var(--accent-color)' : '3px solid transparent';
});
}
_renderPreview(path, content) {
const preview = document.getElementById('filemgr-preview');
if (!preview) return;
// Re-read content on each preview call
if (content === undefined) {
content = this.terminal.fs.readFile(path);
}
// For raw mode, show raw text regardless of type
if (this._rawMode && typeof content === 'string') {
preview.innerHTML = 'RAW: ' + this._escapeHtml(path) + '
' + this._escapeHtml(content.substring(0, 10000)) + '
';
this._updateStatus('Preview: ' + path);
return;
}
// Check file size for text files
const type = this._detectType(path);
const textTypes = ['md', 'text', 'json', 'code', 'csv', 'other'];
if (textTypes.includes(type) && typeof content === 'string' && content.length > 51200) {
content = content.substring(0, 51200);
preview.innerHTML = this._getPreviewHtml(path, content);
const truncatedMsg = document.createElement('div');
truncatedMsg.style.cssText = 'color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;';
truncatedMsg.textContent = '(truncated — file exceeds 50KB)';
preview.appendChild(truncatedMsg);
this._updateStatus('Preview: ' + path);
return;
}
preview.innerHTML = this._getPreviewHtml(path, content);
this._updateStatus('Preview: ' + path);
}
_renderPreviewError(msg) {
const preview = document.getElementById('filemgr-preview');
if (!preview) return;
preview.innerHTML = '' + this._escapeHtml(msg) + '
';
this._updateStatus('Error');
}
_clearPreview() {
const preview = document.getElementById('filemgr-preview');
if (preview) preview.innerHTML = '';
}
_updateStatus(msg) {
const status = document.getElementById('filemgr-status');
if (status) status.textContent = msg;
}
_actionDelete() {
if (!this._selectedFile) {
this._updateStatus('No file selected to delete');
return;
}
if (!confirm('Delete ' + this._selectedFile + '?')) return;
const ok = this.terminal.fs.deleteFile(this._selectedFile);
if (ok) {
this._selectedFile = null;
this._refreshFileList();
this._clearPreview();
this._updateStatus('✅ Deleted');
} else {
this._updateStatus('❌ Failed to delete');
}
}
_actionDownload() {
if (!this._selectedFile) {
this._updateStatus('No file selected to download');
return;
}
const content = this.terminal.fs.readFile(this._selectedFile);
if (content === null || content === false) {
this._updateStatus('❌ File not found');
return;
}
const fileName = this._selectedFile.split('/').pop() || 'file';
const blob = new Blob([content], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this._updateStatus('⬇ Downloaded: ' + fileName);
}
_actionEdit() {
if (!this._selectedFile) {
this._updateStatus('No file selected to edit');
return;
}
if (typeof webx11 !== 'undefined' && webx11 && webx11.executeCommand) {
webx11.executeCommand('edit ' + this._selectedFile);
this._updateStatus('✏ Opened in editor: ' + this._selectedFile);
} else {
this._updateStatus('❌ Editor not available');
}
}
_togglePreviewMode() {
this._rawMode = !this._rawMode;
const btn = document.getElementById('filemgr-toggle-raw');
if (btn) {
btn.textContent = this._rawMode ? '📄 Rendered' : '📄 Raw';
btn.style.borderColor = this._rawMode ? 'var(--accent-color)' : 'var(--border-color)';
btn.style.color = this._rawMode ? 'var(--accent-color)' : 'var(--text-muted)';
}
if (this._selectedFile) {
const content = this.terminal.fs.readFile(this._selectedFile);
this._renderPreview(this._selectedFile, content);
}
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/chatstudio/newos4/modules/ext/filemgr.js');
}