class EditorModule { constructor() { this.commands = {}; this.terminal = null; this._vfsPath = null; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'edit': 'Open the code editor for a VFS file (usage: edit )', }; } registerCommands() { this.commands['edit'] = (args) => this.edit(args); } edit(args) { if (!args[0]) return this.terminal.print('Usage: edit ', 'warning'); const path = args.join(' '); const modal = document.getElementById('code-editor'); const title = document.getElementById('code-editor-title'); const textarea = document.getElementById('code-editor-textarea'); const gutter = document.getElementById('code-editor-gutter'); const statusEl = document.getElementById('code-editor-status'); if (!modal || !textarea) return this.terminal.print('Editor modal not found', 'error'); // Read existing file content or use empty for new files const content = this.terminal.fs.readFile(path); const fileContent = (content !== null && content !== false) ? content : ''; // Set up modal this._vfsPath = path; title.textContent = '๐Ÿ“ Editing: ' + path; textarea.value = fileContent; modal.style.display = 'flex'; textarea.focus(); // Editor enhancements: line numbers, Tab, Ctrl+S const updateGutter = () => { const lines = textarea.value.split('\n').length; let html = ''; for (let i = 1; i <= lines; i++) html += i + '\n'; gutter.textContent = ''; gutter.textContent = html; }; const syncScroll = () => { gutter.scrollTop = textarea.scrollTop; }; const updateStatus = () => { const lines = textarea.value.split('\n').length; const chars = textarea.value.length; statusEl.textContent = `${lines} lines ยท ${chars} chars`; }; updateGutter(); updateStatus(); textarea.onscroll = syncScroll; textarea.oninput = () => { updateGutter(); updateStatus(); }; textarea.onkeydown = (e) => { if (e.key === 'Tab') { e.preventDefault(); insertTab(textarea, e.shiftKey); updateGutter(); } if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveBtn.click(); } }; function insertTab(el, shift) { const s = el.selectionStart, e2 = el.selectionEnd; if (shift) { const before = el.value.substring(0, s); const lineStart = before.lastIndexOf('\n') + 1; if (lineStart < s && el.value.substring(lineStart, lineStart + 1) === '\t') { el.value = el.value.substring(0, lineStart) + el.value.substring(lineStart + 1); el.selectionStart = el.selectionEnd = s - 1; } } else { el.value = el.value.substring(0, s) + '\t' + el.value.substring(e2); el.selectionStart = el.selectionEnd = s + 1; } } // Fullscreen toggle window._editorToggleFullscreen = () => { const content = modal.querySelector('.modal-content'); if (content.style.maxWidth === '100vw') { content.style.maxWidth = '90vw'; content.style.maxHeight = '90vh'; content.style.width = '900px'; content.style.height = '80vh'; } else { content.style.maxWidth = '100vw'; content.style.maxHeight = '100vh'; content.style.width = '100vw'; content.style.height = '100vh'; } }; // Override save button to write back to VFS const saveBtn = modal.querySelector('.modal-footer .btn:last-child'); const cancelBtn = modal.querySelector('.modal-footer .btn-danger'); if (saveBtn) { saveBtn._origOnclick = saveBtn.onclick; saveBtn.onclick = async () => { const code = textarea.value; const ok = this.terminal.fs.writeFile(this._vfsPath, code); if (ok) { this.terminal.print('โœ… Saved to ' + this._vfsPath, 'success'); // Mirror to _mods/ for test_module compatibility const modMatch = this._vfsPath.match(/(?:^|\/)(\w+)\.js$/); if (modMatch) { const modName = modMatch[1]; this.terminal.fs.mkdirRecursive('_mods'); this.terminal.fs.writeFile('_mods/' + modName + '.js', code); if (!this.terminal.fs.readFile('_mods/' + modName + '.orig.js')) { this.terminal.fs.writeFile('_mods/' + modName + '.orig.js', 'placeholder'); } this.terminal.print('๐Ÿ“‹ Mirrored to _mods/' + modName + '.js for live testing', 'info'); } // Notify tutorial module for quest 4 (edit + save) if (this.terminal.tutorialModule) { this.terminal.tutorialModule.onFileSaved(this._vfsPath); } } else { this.terminal.print('โŒ Failed to save ' + this._vfsPath, 'error'); } modal.style.display = 'none'; this._vfsPath = null; // Restore original onclick handler if (saveBtn._origOnclick) saveBtn.onclick = saveBtn._origOnclick; delete saveBtn._origOnclick; }; } if (cancelBtn) { cancelBtn._origOnclick = cancelBtn.onclick; cancelBtn.onclick = () => { modal.style.display = 'none'; this._vfsPath = null; if (cancelBtn._origOnclick) cancelBtn.onclick = cancelBtn._origOnclick; delete cancelBtn._origOnclick; }; } } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new EditorModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/editor.js'); }