// Critical ext module bundle// Generated from /var/www/webx11.duckdns.org/spaces/newos4/modules/ext /* === editor.js === */ 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'); } /* === shell.js === */ class ShellModule { constructor() { this.commands = {}; this.terminal = null; this._vars = { HOME: '/', USER: 'darkbot', SHELL: 'webx11-shell', TERM: 'xterm-256color', PWD: '/' }; this._scriptDepth = 0; this._maxScriptDepth = 3; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'export': 'export = โ€” Set a shell variable', 'unset': 'unset โ€” Remove a shell variable', 'script': 'script โ€” Run a shell script from VFS', '$': '$ โ€” Execute with shell parsing (default: auto-detected)', 'sh': 'sh โ€” Run a shell script from VFS (alias for script)', 'set': 'set โ€” List all shell variables', }; } registerCommands() { this.commands['export'] = (args) => this._cmdExport(args); this.commands['unset'] = (args) => this._cmdUnset(args); this.commands['script'] = (args) => this._cmdScript(args); this.commands['sh'] = (args) => this._cmdScript(args); this.commands['set'] = () => this._cmdSet(); } // โ”€โ”€ Hook into terminal.executeCommand โ”€โ”€ init() { // Guard: prevent double-wrapping if init is called multiple times if (this._initialized) return; this._initialized = true; // Wrap executeCommand to add shell parsing โ€” only for compound commands const origExecute = this.terminal.executeCommand.bind(this.terminal); const self = this; this.terminal.executeCommand = async function(rawInput) { if (!rawInput.trim()) return; this.markDirty(); this.printUserPrompt(rawInput); // Save to command history (preserved from original executeCommand) const cmdRaw = rawInput.trim(); if (this.history.length === 0 || this.history[this.history.length - 1] !== cmdRaw) { this.history.push(cmdRaw); } if (this.history.length > this._historyCap) { this.history = this.history.slice(-this._historyCap); } this.historyIndex = this.history.length; this._saveCommandHistory(); // For simple single-word commands without special chars, skip shell parsing if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) { try { return await this.security.checkAndRun(rawInput); } catch(e) { this.print('Error: ' + e.message, 'error'); return; } } // Substitute variables in the raw input const substituted = self._substituteVars(rawInput); // Split on ; first (sequence) const seqParts = self._splitSequence(substituted); for (const seqPart of seqParts) { if (!seqPart.trim()) continue; // Split on && and || โ€” operator now attached to the FOLLOWING segment const chainParts = self._splitChain(seqPart.trim()); let lastOk = true; for (const chainPart of chainParts) { if (!chainPart.cmd.trim()) continue; const { cmd: chainCmd, operator } = chainPart; // Check the operator that PRECEDES this command if (operator === '&&' && !lastOk) continue; if (operator === '||' && lastOk) continue; // Check for redirects const redirectInfo = self._extractRedirects(chainCmd); if (redirectInfo.hasRedirect) { // Run, capture, write to file await self._executeWithRedirect(redirectInfo); lastOk = redirectInfo.exitOk; } else { // Normal or piped command const pipeParts = this.splitPipe(chainCmd); if (pipeParts.length > 1) { await this.executePipeChain(pipeParts); } else { // Single command โ€” check for shell built-ins first const parts = chainCmd.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); if (self.commands[cmd]) { try { this.markDirty(); await self.commands[cmd](args); lastOk = true; } catch(e) { this.print('Error: ' + e.message, 'error'); lastOk = false; } } else { try { await this.security.checkAndRun(chainCmd); lastOk = true; } catch(e) { this.print('Error: ' + e.message, 'error'); lastOk = false; } } } } } } }; this.terminal._shellModule = this; // Modify pipe chain to add pipe-aware injection and security checks const origPipeChain = this.terminal.executePipeChain.bind(this.terminal); this.terminal.executePipeChain = async function(pipeParts) { // Write pipe input from previous segment as temp file for next segment const shellMod = this._shellModule; let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const cmdText = pipeParts[i]; const parts = cmdText.split(/\s+/); const cmd = parts[0].toLowerCase(); let args = parts.slice(1); // Security gate โ€” replicate the original executePipeChain checks const cls = this.security._classify(cmdText); if (cls.destructive && this.security.level > 0) { this.print(`โš ๏ธ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); return; } if (cls.system && this.security.level > 0) { this.print(`โš ๏ธ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error'); return; } if (!cls.known && this.security.level >= 2) { this.print(`โš ๏ธ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error'); return; } this._pipeInput = pipeInput; if (pipeInput !== null) { // Write pipe input as temp file so file-reading commands can use it this.fs.writeFile('__pipebuf__', pipeInput); // Inject __pipebuf__ as file arg if command is pipe-aware (reads files for input) if (shellMod && shellMod._isPipeAware(cmd)) { // Don't inject if command already has enough file args if (!args.some(a => !a.startsWith('-') && a !== '|' && a !== '>')) { args = args.concat('__pipebuf__'); } } } try { if (i < pipeParts.length - 1) { pipeInput = await this.capturePipeOutput(cmd, args); } else { await this.dispatchCommand(cmd, args); } } finally { // Clean up temp file if (this.fs.readFile('__pipebuf__') !== null) { delete this.fs.root.content['__pipebuf__']; } } } this._pipeInput = null; }; } // โ”€โ”€ Tokenizer helpers โ”€โ”€ _splitSequence(input) { // Split on ; (not inside quotes) const parts = []; let current = ''; let inQuote = false; let quoteChar = ''; for (const ch of input) { if ((ch === '"' || ch === "'") && !inQuote) { inQuote = true; quoteChar = ch; current += ch; } else if (ch === quoteChar && inQuote) { inQuote = false; quoteChar = ''; current += ch; } else if (ch === ';' && !inQuote) { parts.push(current.trim()); current = ''; } else { current += ch; } } parts.push(current.trim()); return parts; } _splitChain(input) { // Split on && and ||, preserving operator info // Operator is attached to the FOLLOWING segment (the one it guards) const parts = []; let current = ''; let inQuote = false; let quoteChar = ''; let pendingOp = null; // operator from previous split, applies to next segment let i = 0; while (i < input.length) { const ch = input[i]; if ((ch === '"' || ch === "'") && !inQuote) { inQuote = true; quoteChar = ch; current += ch; i++; } else if (ch === quoteChar && inQuote) { inQuote = false; quoteChar = ''; current += ch; i++; } else if (!inQuote && input.substring(i, i+2) === '&&') { parts.push({ cmd: current.trim(), operator: pendingOp }); pendingOp = '&&'; current = ''; i += 2; } else if (!inQuote && input.substring(i, i+2) === '||') { parts.push({ cmd: current.trim(), operator: pendingOp }); pendingOp = '||'; current = ''; i += 2; } else { current += ch; i++; } } parts.push({ cmd: current.trim(), operator: pendingOp }); return parts; } _extractRedirects(input) { // Find >, >>, < outside quotes let cmd = ''; let hasRedirect = false; let redirectOp = ''; let redirectFile = ''; let append = false; let inQuote = false, quoteChar = ''; let i = 0; const parts = []; let current = ''; while (i < input.length) { const ch = input[i]; if ((ch === '"' || ch === "'") && !inQuote) { inQuote = true; quoteChar = ch; current += ch; i++; } else if (ch === quoteChar && inQuote) { inQuote = false; quoteChar = ''; current += ch; i++; } else if (!inQuote) { if (input.substring(i, i+2) === '>>') { if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '>>'; append = true; i += 2; // Read until next redirect or end let file = ''; while (i < input.length && input[i] === ' ') i++; while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') { file += input[i]; i++; } redirectFile = file.trim(); if (i < input.length && (input[i] === '>' || input[i] === '<')) continue; break; } else if (input.substring(i, i+1) === '>') { if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '>'; append = false; i += 1; let file = ''; while (i < input.length && input[i] === ' ') i++; while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') { file += input[i]; i++; } redirectFile = file.trim(); if (i < input.length && (input[i] === '>' || input[i] === '<')) continue; break; } else if (input.substring(i, i+1) === '<') { if (current.trim()) { parts.push(current.trim()); current = ''; } hasRedirect = true; redirectOp = '<'; i += 1; let file = ''; while (i < input.length && input[i] === ' ') i++; while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') { file += input[i]; i++; } redirectFile = file.trim(); if (i < input.length && (input[i] === '>' || input[i] === '<')) continue; break; } else { current += ch; i++; } } else { current += ch; i++; } } if (current.trim()) parts.push(current.trim()); const cmdText = parts.join(' '); return { hasRedirect, redirectOp, redirectFile, append, cmdText, exitOk: true }; } async _executeWithRedirect(redirectInfo) { const { redirectOp, redirectFile, append, cmdText } = redirectInfo; const fs = this.terminal.fs; if (redirectOp === '<') { // Read file content as stdin const inputContent = fs.readFile(redirectFile); if (inputContent === null) { this.terminal.print(`shell: ${redirectFile}: No such file`, 'error'); redirectInfo.exitOk = false; return; } this.terminal._pipeInput = inputContent; try { const parts = cmdText.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // If command reads files, inject pipe content as temp file if (this._isPipeAware(cmd)) { fs.writeFile('__pipebuf__', inputContent); const newArgs = [...args]; if (!newArgs.some(a => !a.startsWith('-'))) { newArgs.push('__pipebuf__'); } try { await this.terminal.dispatchCommand(cmd, newArgs); } finally { if (fs.readFile('__pipebuf__') !== null) { delete fs.root.content['__pipebuf__']; } } } else { await this.terminal.dispatchCommand(cmd, args); } redirectInfo.exitOk = true; } catch(e) { this.terminal.print('Error: ' + e.message, 'error'); redirectInfo.exitOk = false; } finally { this.terminal._pipeInput = null; } return; } // > or >> โ€” capture output, write to file let output = ''; this.terminal._pushPrintCapture(buf => { output += buf; }); let cmdOk = true; try { // Check for pipes within the command before redirect const pipeParts = this.terminal.splitPipe(cmdText); if (pipeParts.length > 1) { // Execute pipe chain manually to capture all output let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const pText = pipeParts[i]; const pParts = pText.split(/\s+/); const pCmd = pParts[0].toLowerCase(); const pArgs = pParts.slice(1); if (pipeInput !== null) { fs.writeFile('__pipebuf__', pipeInput); } if (i < pipeParts.length - 1) { let capt = ''; this.terminal._pushPrintCapture(buf => { capt += buf; }); try { await this.terminal.dispatchCommand(pCmd, pArgs); } finally { this.terminal._popPrintCapture(); } pipeInput = capt.trim(); } else { await this.terminal.dispatchCommand(pCmd, pArgs); } if (fs.readFile('__pipebuf__') !== null) { delete fs.root.content['__pipebuf__']; } } } else { const parts = cmdText.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); await this.terminal.dispatchCommand(cmd, args); } } catch(e) { cmdOk = false; // Pop capture before showing the error this.terminal._popPrintCapture(); this.terminal.print('Error: ' + e.message, 'error'); redirectInfo.exitOk = false; return; } finally { // Always pop capture โ€” critical: without this, terminal output stays captured // (empty stack = normal DOM printing, so pop is idempotent-safe when already popped) if (this.terminal._printCaptureStack && this.terminal._printCaptureStack.length > 0) { // popped in catch path for error case; success path pops here this.terminal._popPrintCapture(); } } const captured = output.trimEnd(); if (captured) { if (append) { const existing = fs.readFile(redirectFile) || ''; fs.writeFile(redirectFile, existing + (existing ? '\n' : '') + captured); } else { fs.writeFile(redirectFile, captured); } this.terminal.print(`Redirected ${captured.split('\n').length} line(s) to ${redirectFile}`, 'success'); } else { // Write empty file if (!append) { fs.writeFile(redirectFile, ''); this.terminal.print(`Redirected (empty) to ${redirectFile}`, 'success'); } } redirectInfo.exitOk = cmdOk; } _isPipeAware(cmd) { // Commands that read from files and should use pipe buffer return ['grep', 'head', 'tail', 'sort', 'wc', 'cut', 'uniq', 'expand'].includes(cmd); } // โ”€โ”€ Variable substitution โ”€โ”€ _substituteVars(input) { // Replace $VAR and ${VAR} (not inside single quotes) let result = ''; let inSingle = false; let i = 0; while (i < input.length) { const ch = input[i]; if (ch === "'") { inSingle = !inSingle; result += ch; i++; } else if (ch === '$' && !inSingle && i + 1 < input.length) { // Check for $( if (input[i+1] === '(') { result += ch; i++; continue; } // Check for ${VAR} if (input[i+1] === '{') { let end = input.indexOf('}', i); if (end !== -1) { const varName = input.substring(i+2, end); const val = this._getVar(varName); result += val !== undefined ? val : '$' + varName; i = end + 1; continue; } } // Check for $VAR (alphanumeric/underscore) let j = i + 1; if (j < input.length && /[A-Za-z_]/.test(input[j])) { while (j < input.length && /\w/.test(input[j])) j++; const varName = input.substring(i+1, j); const val = this._getVar(varName); result += val !== undefined ? val : ''; i = j; continue; } // $ followed by digit โ€” not a var result += ch; i++; } else { result += ch; i++; } } return result; } _getVar(name) { if (name === 'PWD') return '/' + this.terminal.fs.currentPath.join('/'); return this._vars[name] !== undefined ? this._vars[name] : undefined; } _setVar(name, value) { this._vars[name] = value; } // โ”€โ”€ Commands โ”€โ”€ _cmdExport(args) { if (!args[0] || !args[0].includes('=')) { return this.terminal.print('Usage: export =', 'warning'); } const eqIdx = args[0].indexOf('='); const name = args[0].substring(0, eqIdx); let value = args[0].substring(eqIdx + 1); // Support quoted values: export FOO="hello world" if (args.length > 1) { const rest = args.slice(1).join(' '); if (value.endsWith('"') || value.endsWith("'")) { // First part ended quote โ€” fine } else if (value.startsWith('"') || value.startsWith("'")) { value = value + (rest ? ' ' + rest : ''); } } // Strip surrounding quotes if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.substring(1, value.length - 1); } this._setVar(name, value); this.terminal.print(`Shell ${name}=${value}`, 'success'); } _cmdUnset(args) { if (!args[0]) return this.terminal.print('Usage: unset ', 'warning'); delete this._vars[args[0]]; this.terminal.print(`Shell var removed: ${args[0]}`, 'info'); } _cmdSet() { const fs = this.terminal.fs; this.terminal.print('Shell variables:', 'info'); for (const [k, v] of Object.entries(this._vars)) { this.terminal.print(`${k}=${v}`, 'info'); } this.terminal.print(`PWD=/${fs.currentPath.join('/')}`, 'info'); } async _cmdScript(args) { if (!args[0]) return this.terminal.print('Usage: script ', 'warning'); if (this._scriptDepth >= this._maxScriptDepth) { return this.terminal.print(`script: max recursion depth (${this._maxScriptDepth}) reached`, 'error'); } const path = args[0]; const content = this.terminal.fs.readFile(path); if (content === null) return this.terminal.print(`script: ${path}: No such file`, 'error'); this._scriptDepth++; try { this.terminal.print(`Running script: ${path}`, 'info'); const lines = content.split('\n'); let lineNum = 0; for (const rawLine of lines) { lineNum++; const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt'); const substituted = this._substituteVars(line); const seqParts = this._splitSequence(substituted); for (const seqPart of seqParts) { if (!seqPart.trim()) continue; const chainParts = this._splitChain(seqPart.trim()); let lastOk = true; for (const chainPart of chainParts) { if (!chainPart.cmd.trim()) continue; if (chainPart.operator === '&&' && !lastOk) continue; if (chainPart.operator === '||' && lastOk) continue; const redirectInfo = this._extractRedirects(chainPart.cmd); if (redirectInfo.hasRedirect) { await this._executeWithRedirect(redirectInfo); lastOk = redirectInfo.exitOk; } else { const pipeParts = this.terminal.splitPipe(chainPart.cmd); if (pipeParts.length > 1) { await this.terminal.executePipeChain(pipeParts); } else { const parts = chainPart.cmd.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // Check if it's a shell built-in command if (this.commands[cmd]) { try { this.terminal.markDirty(); await this.commands[cmd](args); lastOk = true; } catch(e) { this.terminal.print('Error: ' + e.message, 'error'); lastOk = false; } } else { try { await this.terminal.security.checkAndRun(chainPart.cmd); lastOk = true; } catch(e) { this.terminal.print('Error: ' + e.message, 'error'); lastOk = false; } } } } } } } this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success'); } finally { // Always decrement โ€” even on error or thrown exception this._scriptDepth--; } } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new ShellModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/shell.js'); } /* === alias.js === */ class AliasModule { constructor() { this.commands = {}; this.terminal = null; this.key = 'webx11_aliases'; this.defaults = { 'll': 'ls', 'h': 'help', 'cls': 'clear', 'sh': 'script' }; this.aliases = this.load(); this._aliasDepth = 0; this._aliasMaxDepth = 10; } getHelpInfo() { return { 'alias': 'List, set, or show aliases (usage: alias [name=value])', 'unalias': 'Remove a user-defined alias (usage: unalias )', }; } load() { try { const data = localStorage.getItem(this.key); return data ? { ...this.defaults, ...JSON.parse(data) } : { ...this.defaults }; } catch (e) { return { ...this.defaults }; } } save() { const custom = {}; Object.keys(this.aliases).forEach(k => { if (!this.defaults[k]) custom[k] = this.aliases[k]; }); localStorage.setItem(this.key, JSON.stringify(custom)); } registerCommands() { const self = this; // Register management commands this.commands['alias'] = (args) => self.handleAlias(args); this.commands['unalias'] = (args) => self.handleUnalias(args); // Register each alias as a delegating command Object.keys(this.aliases).forEach(name => { if (name === 'alias' || name === 'unalias') return; const target = this.aliases[name]; this.commands[name] = (args) => { if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; } this._aliasDepth++; try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); } finally { this._aliasDepth--; } }; }); } handleAlias(args) { if (!args[0]) { // List all aliases this.terminal.print('=== Aliases ===', 'info'); const keys = Object.keys(this.aliases); if (keys.length === 0) { this.terminal.print(' No aliases defined', 'warning'); } else { keys.forEach(name => { const mark = this.defaults[name] ? ' (built-in)' : ''; this.terminal.print(` ${name}=${this.aliases[name]}${mark}`); }); } this.terminal.print('Usage: alias <name>=<value>', 'info'); return; } // Parse name=value const input = args.join(' '); const eqIdx = input.indexOf('='); if (eqIdx === -1) { // Show specific alias value const name = args[0]; if (this.aliases[name]) { const mark = this.defaults[name] ? ' (built-in)' : ''; this.terminal.print(`${name}=${this.aliases[name]}${mark}`, 'info'); } else { this.terminal.print(`Alias '${name}' not found`, 'error'); } return; } const name = input.substring(0, eqIdx).trim(); const value = input.substring(eqIdx + 1).trim(); if (!name) return this.terminal.print('Invalid alias name', 'error'); if (!value) return this.terminal.print('Invalid alias value', 'error'); if (name === 'alias' || name === 'unalias') { return this.terminal.print('Cannot alias reserved command name', 'error'); } if (this.defaults[name]) { return this.terminal.print(`Cannot alias built-in alias '${name}'`, 'warning'); } this.aliases[name] = value; this.save(); // Register the new alias as a command immediately const target = value; this.commands[name] = (args) => { if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; } this._aliasDepth++; try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); } finally { this._aliasDepth--; } }; this.terminal.print(`Alias set: ${name}=${value}`, 'success'); } handleUnalias(args) { if (!args[0]) return this.terminal.print('Usage: unalias <name>', 'warning'); const name = args[0]; if (this.defaults[name]) { return this.terminal.print(`Cannot unalias built-in alias '${name}'`, 'warning'); } if (!this.aliases[name]) { return this.terminal.print(`Alias '${name}' not found`, 'error'); } delete this.aliases[name]; delete this.commands[name]; this.save(); this.terminal.print(`Alias removed: ${name}`, 'success'); } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new AliasModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/alias.js'); } /* === finder.js === */ class FinderModule { constructor() { this.commands = {}; this.terminal = null; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'find': 'Recursively search VFS files/directories by name (usage: find )', }; } registerCommands() { this.commands['find'] = (args) => this.find(args); } find(args) { if (!args[0]) return this.terminal.print('Usage: find โ€” recursively searches VFS files/dirs matching query', 'warning'); const query = args.join(' ').toLowerCase(); const results = []; this._recurse(this.terminal.fs.root, [], query, results); if (results.length === 0) return this.terminal.print(`No matches for "${query}"`, 'warning'); this.terminal.print(`=== Found ${results.length} match(es) for "${query}" ===`, 'info'); results.forEach(r => { const icon = r.type === 'dir' ? '๐Ÿ“' : '๐Ÿ“„'; this.terminal.print(` ${icon} ${r.path}`); }); } _recurse(node, pathParts, query, results) { if (!node || !node.content) return; for (const [name, child] of Object.entries(node.content)) { const fullPath = '/' + [...pathParts, name].join('/'); if (name.toLowerCase().includes(query)) { results.push({ name, path: fullPath, type: child.type || (child.content ? 'dir' : 'file') }); } if (child.content && typeof child.content === 'object') { this._recurse(child, [...pathParts, name], query, results); } } } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new FinderModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/finder.js'); } /* === filemgr.js === */ 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._multiSelect = new Set(); // v4.5.78: multi-select for batch ops this._clipboard = { mode: null, paths: [] }; // 'copy' or 'cut' 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 โ€” v4.5.67: sanitize URL to prevent javascript: scheme injection html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => { const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#'; return `${text}`; }); // 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(/)<[^<]*)*<\/script>/gi, '') .replace(/on\w+\s*=\s*"[^"]*"/gi, '') .replace(/on\w+\s*=\s*'[^']*'/gi, ''); return '
    ' + '
    '; } 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 += ''; html += ''; }); html += '
    ' + lineNum + '' + (escaped || ' ') + '
    '; if (totalLines > maxLines) { html += '
    (showing ' + maxLines + ' of ' + totalLines + ' lines)
    '; } return html; } if (type === 'image') { // VFS stores images as base64 strings โ€” detect and render inline const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); if (isBase64) { const mimeTypes = {jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp',bmp:'image/bmp',ico:'image/x-icon'}; const mime = mimeTypes[ext] || 'image/png'; return '
    ' + this._escapeHtml(fileName) + '
    '; } return '
    ๐Ÿ“ท Image โ€” cannot render, file may be stored as binary
    '; } 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') { const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); if (isBase64) { const mimeTypes = {mp4:'video/mp4',webm:'video/webm',avi:'video/x-msvideo',mov:'video/quicktime',mkv:'video/x-matroska'}; const mime = mimeTypes[ext] || 'video/mp4'; return '
    '; } return '
    ๐ŸŽฌ Video โ€” cannot render
    '; } if (type === 'audio') { const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); if (isBase64) { const mimeTypes = {mp3:'audio/mpeg',wav:'audio/wav',ogg:'audio/ogg',flac:'audio/flac',aac:'audio/aac',m4a:'audio/mp4'}; const mime = mimeTypes[ext] || 'audio/mpeg'; return '
    '; } return '
    ๐ŸŽต Audio โ€” cannot render
    '; } if (type === 'pdf') { const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100)); if (isBase64) { return '
    '; } return '
    ๐Ÿ“„ PDF โ€” cannot render
    '; } 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 += ''; }); html += ''; rows.slice(1).forEach(row => { html += ''; row.forEach(cell => { html += ''; }); html += ''; }); html += '
    ' + this._escapeHtml(cell) + '
    ' + this._escapeHtml(cell) + '
    '; 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;height:100vh;'; // 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; flex: 1 !important; min-height: 0; } #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); // v4.5.78: Batch operation buttons const batchBar = document.createElement('div'); batchBar.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;'; const mkBtn = (label, title, action) => { const b = document.createElement('button'); b.textContent = label; b.title = title; b.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 8px;border-radius:4px;font-family:inherit;font-size:0.85em;min-height:32px;touch-action:manipulation;'; b.onclick = () => action(); return b; }; batchBar.appendChild(mkBtn('๐Ÿ“‹ Copy', 'Copy selected files', () => this._batchCopy())); batchBar.appendChild(mkBtn('โœ‚๏ธ Cut', 'Cut selected files', () => this._batchCut())); batchBar.appendChild(mkBtn('๐Ÿ“ฅ Paste', 'Paste from clipboard', () => this._batchPaste())); batchBar.appendChild(mkBtn('๐Ÿ—‘๏ธ Del', 'Delete selected files', () => this._batchDelete())); batchBar.appendChild(mkBtn('๐Ÿ“ Rename', 'Rename selected file', () => this._batchRename())); batchBar.appendChild(mkBtn('โ˜ All', 'Toggle select all', () => this._toggleSelectAll())); toolbar.appendChild(batchBar); 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 = 'flex:1;overflow:hidden;display:flex;min-height:0;'; // Sidebar const sidebar = document.createElement('div'); sidebar.id = 'filemgr-sidebar'; sidebar.style.cssText = 'width:25%;min-width:150px;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 encryptBtn = document.createElement('button'); encryptBtn.textContent = '๐Ÿ” Encrypt'; encryptBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--warning-color);color:var(--warning-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; encryptBtn.onclick = () => this._actionEncrypt(); actionBar.appendChild(encryptBtn); const compressBtn = document.createElement('button'); compressBtn.textContent = '๐Ÿ“ฆ Zip'; compressBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--info-color);color:var(--info-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; compressBtn.onclick = () => this._actionCompress(); actionBar.appendChild(compressBtn); 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 newFileBtn = document.createElement('button'); newFileBtn.textContent = '๐Ÿ“„ New'; newFileBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--success-color);color:var(--success-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;'; newFileBtn.onclick = () => this._actionNewFile(); actionBar.appendChild(newFileBtn); 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; const isMulti = this._multiSelect.has(fullPath); item.dataset.path = fullPath; item.dataset.type = 'file'; item.dataset.selected = (isSelected || isMulti) ? 'true' : 'false'; const bgColor = (isSelected || isMulti) ? '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 || isMulti) { 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 = (e) => { // v4.5.78: Ctrl+click or checkbox for multi-select if (e.ctrlKey || e.metaKey) { if (this._multiSelect.has(fullPath)) this._multiSelect.delete(fullPath); else this._multiSelect.add(fullPath); this._refreshFileList(); return; } 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 || this._multiSelect.has(item.dataset.path); 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'; }); } // v4.5.78: Batch operations _getSelectedPaths() { const paths = [...this._multiSelect]; if (this._selectedFile && !paths.includes(this._selectedFile)) paths.push(this._selectedFile); return paths; } _batchCopy() { const paths = this._getSelectedPaths(); if (!paths.length) { this._updateStatus('No files selected'); return; } this._clipboard = { mode: 'copy', paths: [...paths] }; this._multiSelect.clear(); this._updateStatus('๐Ÿ“‹ Copied ' + paths.length + ' item(s)'); } _batchCut() { const paths = this._getSelectedPaths(); if (!paths.length) { this._updateStatus('No files selected'); return; } this._clipboard = { mode: 'cut', paths: [...paths] }; this._multiSelect.clear(); this._updateStatus('โœ‚๏ธ Cut ' + paths.length + ' item(s)'); } _batchPaste() { if (!this._clipboard.paths.length) { this._updateStatus('Clipboard empty'); return; } const dest = '/' + this.currentPath.join('/'); let count = 0; this._clipboard.paths.forEach(src => { const name = src.split('/').pop(); const destPath = dest === '/' ? '/' + name : dest + '/' + name; try { if (this._clipboard.mode === 'copy') { const content = this.terminal.fs.readFile(src); if (content !== null) { this.terminal.fs.writeFile(destPath, content); count++; } } else if (this._clipboard.mode === 'cut') { const content = this.terminal.fs.readFile(src); if (content !== null) { this.terminal.fs.writeFile(destPath, content); this.terminal.fs.deleteFile(src); count++; } } } catch (e) { /* skip errors */ } }); if (this._clipboard.mode === 'cut') this._clipboard = { mode: null, paths: [] }; this._multiSelect.clear(); this._refreshFileList(); this._updateStatus('๐Ÿ“ฅ Pasted ' + count + ' item(s)'); } _batchDelete() { const paths = this._getSelectedPaths(); if (!paths.length) { this._updateStatus('No files selected'); return; } if (!confirm('Delete ' + paths.length + ' file(s)?')) return; let count = 0; paths.forEach(p => { try { this.terminal.fs.deleteFile(p); count++; } catch (e) { /* skip */ } }); this._multiSelect.clear(); this._selectedFile = null; this._refreshFileList(); this._updateStatus('๐Ÿ—‘๏ธ Deleted ' + count + ' file(s)'); } _batchRename() { const path = this._selectedFile || (this._multiSelect.size === 1 ? [...this._multiSelect][0] : null); if (!path) { this._updateStatus('Select one file to rename'); return; } const oldName = path.split('/').pop(); const newName = prompt('Rename to:', oldName); if (!newName || newName === oldName) return; const dir = path.substring(0, path.lastIndexOf('/')) || '/'; const newPath = dir === '/' ? '/' + newName : dir + '/' + newName; try { const content = this.terminal.fs.readFile(path); if (content === null) { this._updateStatus('Cannot read file'); return; } this.terminal.fs.writeFile(newPath, content); this.terminal.fs.deleteFile(path); this._selectedFile = newPath; this._multiSelect.clear(); this._refreshFileList(); this._updateStatus('๐Ÿ“ Renamed to ' + newName); } catch (e) { this._updateStatus('Rename failed: ' + e.message); } } _toggleSelectAll() { const sidebar = document.getElementById('filemgr-sidebar'); if (!sidebar) return; const fileItems = sidebar.querySelectorAll('[data-type="file"]'); if (this._multiSelect.size >= fileItems.length) { this._multiSelect.clear(); } else { fileItems.forEach(item => this._multiSelect.add(item.dataset.path)); } this._refreshFileList(); this._updateStatus(this._multiSelect.size + ' selected'); } _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; } const path = this._selectedFile; // Close file manager first so code editor modal (z-index:1000) is visible above this._closeOverlay(); if (typeof webx11 !== 'undefined' && webx11 && webx11.dispatchCommand) { setTimeout(() => { webx11.dispatchCommand('edit', [path]); }, 100); this._updateStatus('โœ Opened in editor: ' + path); } 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); } } _actionEncrypt() { if (!this._selectedFile) { this._updateStatus('No file selected'); return; } if (this._selectedFile.endsWith('.enc')) { this._updateStatus('โš  Already encrypted (.enc)'); return; } const passphrase = prompt('Enter passphrase for AES-256-GCM encryption:'); if (!passphrase || passphrase.length < 4) { this._updateStatus('โŒ Passphrase too short (min 4 chars)'); return; } // Delegate to cryptogzip module's encrypt command if (typeof webx11 !== 'undefined' && webx11.executeCommand) { webx11.executeCommand('encrypt ' + this._selectedFile + ' ' + passphrase); this._updateStatus('๐Ÿ” Encrypted: ' + this._selectedFile + '.enc'); this._refreshFileList(); } } _actionCompress() { if (!this._selectedFile) { this._updateStatus('No file selected'); return; } if (this._selectedFile.endsWith('.gz')) { this._updateStatus('โš  Already compressed (.gz)'); return; } if (typeof webx11 !== 'undefined' && webx11.executeCommand) { webx11.executeCommand('gzip ' + this._selectedFile); this._updateStatus('๐Ÿ“ฆ Compressed: ' + this._selectedFile + '.gz'); this._refreshFileList(); } } _actionNewFile() { const name = prompt('New file name (e.g. myfile.txt):'); if (!name) return; const pathStr = '/' + this.currentPath.join('/'); const cleanPath = pathStr === '/' ? '/' + name : pathStr + '/' + name; const content = prompt('Enter file content (optional):') || ''; const ok = this.terminal.fs.writeFile(cleanPath, content); if (ok) { this._updateStatus('โœ… Created: ' + cleanPath + ' (' + content.length + ' chars)'); this._refreshFileList(); } else { this._updateStatus('โŒ Failed to create file'); } } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js'); } /* === diff.js === */ class DiffModule { constructor() { this.commands = {}; this.terminal = null; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'diff': 'Show differences between two VFS files or compare module source (usage: diff or diff --module )', }; } registerCommands() { this.commands['diff'] = (args) => this.diff(args); } async diff(args) { if (!args.length) return this.terminal.print('Usage: diff or diff --module ', 'warning'); if (args[0] === '--module') { if (!args[1]) return this.terminal.print('Usage: diff --module ', 'warning'); return this._diffModule(args[1]); } return this._diffFiles(args[0], args[1]); } _diffFiles(path1, path2) { let content1, content2; try { content1 = this.terminal.fs.readFile(path1); content2 = this.terminal.fs.readFile(path2); } catch (e) { return this.terminal.print(`Error reading files: ${e.message}`, 'error'); } if (content1 === null || content1 === undefined) { return this.terminal.print(`File not found: ${path1}`, 'error'); } if (content2 === null || content2 === undefined) { return this.terminal.print(`File not found: ${path2}`, 'error'); } const lines1 = String(content1).split('\n'); const lines2 = String(content2).split('\n'); // Strip trailing empty line if file ends with newline if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop(); if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop(); if (lines1.length === 0 && lines2.length === 0) { return this.terminal.print('Both files are empty โ€” no differences', 'info'); } // Compute LCS-based diff const result = this._computeDiff(lines1, lines2); // Render output this.terminal.print(`--- ${path1}`, 'info'); this.terminal.print(`+++ ${path2}`, 'info'); if (result.length === 0) { return this.terminal.print('Files are identical', 'success'); } const maxLineNum = Math.max(lines1.length, lines2.length); const lineNumWidth = String(maxLineNum).length; result.forEach(part => { if (part.type === 'same') { // Print up to 3 unchanged lines for context, then collapse if (part.lines.length <= 3) { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${line}`, 'dim'); }); } else { // Show first line this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim'); this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim'); // Show last line const lastIdx = part.lines.length - 1; const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim'); } } else if (part.type === 'removed') { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(`-${ln} ${line}`, 'error'); }); } else if (part.type === 'added') { part.lines.forEach((line, i) => { const ln = String(part.bStart + i + 1).padStart(lineNumWidth); this.terminal.print(`+${ln} ${line}`, 'success'); }); } }); } _computeDiff(a, b) { // Build LCS table const m = a.length; const n = b.length; const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (a[i - 1] === b[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // Backtrack to build diff parts const parts = []; let i = m, j = n; let removed = [], added = [], same = []; const aStart = 0, bStart = 0; const flush = () => { if (same.length > 0) { parts.push({ type: 'same', aStart: aStart + (parts.length > 0 ? parts.reduce((sum, p) => { if (p.type === 'same') return sum + p.lines.length; if (p.type === 'removed') return sum + p.lines.length; return sum; }, 0) : 0), lines: same.splice(0) }); } if (removed.length > 0 || added.length > 0) { if (removed.length > 0) { const aIdx = parts.reduce((sum, p) => { if (p.type === 'same') return sum + p.lines.length; if (p.type === 'removed') return sum + p.lines.length; return sum; }, 0); parts.push({ type: 'removed', aStart: aIdx, lines: removed.splice(0) }); } if (added.length > 0) { const bIdx = parts.reduce((sum, p) => { if (p.type === 'same') return sum; if (p.type === 'added') return sum + p.lines.length; return sum; }, 0); parts.push({ type: 'added', bStart: bIdx, lines: added.splice(0) }); } } }; // Trace back through DP table const trace = []; while (i > 0 || j > 0) { if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { trace.push({ type: 'same', lineA: a[i - 1], lineB: b[j - 1] }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { trace.push({ type: 'added', lineB: b[j - 1] }); j--; } else { trace.push({ type: 'removed', lineA: a[i - 1] }); i--; } } // Reverse trace to get forward order trace.reverse(); // Build parts from trace let aIdx = 0, bIdx = 0; for (const entry of trace) { if (entry.type === 'same') { same.push(entry.lineA); aIdx++; bIdx++; } else if (entry.type === 'removed') { flush(); removed.push(entry.lineA); aIdx++; } else if (entry.type === 'added') { flush(); added.push(entry.lineB); bIdx++; } } flush(); return parts; } async _diffModule(modName) { // Find module in registry const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); if (!entry) { return this.terminal.print(`Module "${modName}" not found in registry`, 'error'); } const url = entry.url; if (!url) { return this.terminal.print(`No source URL for module "${modName}"`, 'warning'); } // Fetch original source let originalSource; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); originalSource = await resp.text(); } catch (e) { return this.terminal.print(`Failed to fetch original source: ${e.message}`, 'error'); } // Read local edited version const localPath = `_mods/${modName}.js`; let localSource; let hasLocalEdit = false; try { localSource = this.terminal.fs.readFile(localPath); if (localSource === null || localSource === undefined) { hasLocalEdit = false; } else { hasLocalEdit = true; } } catch (e) { hasLocalEdit = false; } if (!hasLocalEdit) { // Try to read the module's currently loaded source const loadedModule = this.terminal.moduleManager.modules[modName]; if (!loadedModule) { return this.terminal.print(`Module "${modName}" is not loaded and has no local edits`, 'info'); } return this.terminal.print(`Module "${modName}" has no local edits โ€” source matches original`, 'success'); } // Compare original source vs local edit const lines1 = String(originalSource).split('\n'); const lines2 = String(localSource).split('\n'); // Strip trailing empty line if file ends with newline if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop(); if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop(); this.terminal.print(`--- ${url}`, 'info'); this.terminal.print(`+++ ${localPath}`, 'info'); if (lines1.length === lines2.length && lines1.every((l, i) => l === lines2[i])) { return this.terminal.print('No differences โ€” local edit matches original', 'success'); } const result = this._computeDiff(lines1, lines2); const maxLineNum = Math.max(lines1.length, lines2.length); const lineNumWidth = String(maxLineNum).length; result.forEach(part => { if (part.type === 'same') { if (part.lines.length <= 3) { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${line}`, 'dim'); }); } else { this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim'); this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim'); const lastIdx = part.lines.length - 1; const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim'); } } else if (part.type === 'removed') { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(`-${ln} ${line}`, 'error'); }); } else if (part.type === 'added') { part.lines.forEach((line, i) => { const ln = String(part.bStart + i + 1).padStart(lineNumWidth); this.terminal.print(`+${ln} ${line}`, 'success'); }); } }); } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new DiffModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/diff.js'); } /* === busybox.js === */ class BusyBoxModule { constructor() { this.commands = {}; this.terminal = null; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'which': 'which โ€” Locate a command in the module registry', 'env': 'env โ€” List environment variables (webx11_ keys + user profile)', 'date': 'date โ€” Show current date/time (ISO, local, UTC, timestamp)', 'cal': 'cal [month] [year] โ€” Display an ASCII calendar', 'yes': 'yes [string] โ€” Output repeated string (default "y", capped at 50)', 'seq': 'seq or seq โ€” Generate sequence of numbers', 'base64': 'base64 [-d] โ€” Base64 encode/decode', 'printf': 'printf [args...] โ€” Formatted print (%s, %d, %f, \\n, \\t)', 'cut': 'cut -f [-d] โ€” Extract column from file', 'uniq': 'uniq โ€” Filter adjacent duplicate lines from a file', 'hostname': 'hostname โ€” Show host/user-agent info', 'whoami': 'whoami โ€” Show current user name', 'basename': 'basename โ€” Strip directory from path', 'dirname': 'dirname โ€” Strip filename from path', 'sleep': 'sleep โ€” Delay execution with countdown', 'expand': 'expand โ€” Convert tabs to spaces (default 4) in file', }; } registerCommands() { this.commands['which'] = (args) => this._cmdWhich(args); this.commands['env'] = () => this._cmdEnv(); this.commands['date'] = (args) => this._cmdDate(args); this.commands['cal'] = (args) => this._cmdCal(args); this.commands['yes'] = (args) => this._cmdYes(args); this.commands['seq'] = (args) => this._cmdSeq(args); this.commands['base64'] = (args) => this._cmdBase64(args); this.commands['printf'] = (args) => this._cmdPrintf(args); this.commands['cut'] = (args) => this._cmdCut(args); this.commands['uniq'] = (args) => this._cmdUniq(args); this.commands['hostname'] = () => this._cmdHostname(); this.commands['whoami'] = () => this._cmdWhoami(); this.commands['basename'] = (args) => this._cmdBasename(args); this.commands['dirname'] = (args) => this._cmdDirname(args); this.commands['sleep'] = (args) => this._cmdSleep(args); this.commands['expand'] = (args) => this._cmdExpand(args); } // โ”€โ”€ which โ”€โ”€ _cmdWhich(args) { if (!args[0]) return this.terminal.print('Usage: which ', 'warning'); const query = args[0].toLowerCase(); const all = this.terminal.moduleManager.getAllCommands(); const keys = Object.keys(all); if (all[query]) { let foundMod = null; for (const modName in this.terminal.moduleManager.modules) { const mod = this.terminal.moduleManager.modules[modName]; if (mod.commands && mod.commands[query]) { foundMod = mod; break; } } const src = foundMod ? ` (from ${foundMod.constructor.name})` : ''; return this.terminal.print(`${query}${src}`, 'info'); } // Fuzzy match: check Levenshtein distance up to 2 let best = null, bestDist = 3; for (const k of keys) { const d = this._levenshtein(query, k); if (d < bestDist) { best = k; bestDist = d; } } if (best) { this.terminal.print(`which: no '${query}' in registry`, 'error'); this.terminal.print(`Did you mean '${best}'?`, 'info'); } else { this.terminal.print(`which: no '${query}' in registry`, 'error'); } } _levenshtein(a, b) { const m = a.length, n = b.length; const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]); } } return dp[m][n]; } // โ”€โ”€ env โ”€โ”€ _cmdEnv() { const lines = []; // webx11_ localStorage keys for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith('webx11_')) { let v = localStorage.getItem(k) || ''; if (v.length > 80) v = v.substring(0, 77) + '...'; lines.push(`${k}=${v}`); } } // Runtime info if (this.terminal.ollamaManager) { lines.push(`OLLAMA_MODEL=${this.terminal.ollamaManager.currentModel || 'none'}`); lines.push(`OLLAMA_CONNECTED=${this.terminal.ollamaManager.connected}`); } lines.push(`PWD=/${this.terminal.fs.currentPath.join('/')}`); lines.push(`SHELL=webx11-terminal`); lines.push(`USER=darkbot`); lines.push(`HOME=/`); lines.push(`TERM=xterm-256color`); const prof = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; if (prof && prof._userProfile) { const facts = prof._userProfile.knownFacts; if (facts) { for (const [k, v] of Object.entries(facts)) { lines.push(`USER_${k.toUpperCase()}=${v}`); } } } this.terminal.print(lines.join('\n'), 'info'); } // โ”€โ”€ date โ”€โ”€ _cmdDate(args) { const n = new Date(); if (args.length > 0) { const flag = args[0].toLowerCase(); if (flag === '-u' || flag === '--utc' || flag === 'utc') return this.terminal.print(n.toUTCString(), 'info'); if (flag === '-i' || flag === '--iso' || flag === 'iso') return this.terminal.print(n.toISOString(), 'info'); if (flag === '-t' || flag === '--ts' || flag === 'timestamp') return this.terminal.print(String(n.getTime()), 'info'); if (flag === '-r' || flag === '--rfc' || flag === 'rfc') return this.terminal.print(n.toUTCString(), 'info'); } const local = n.toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }); this.terminal.print(local, 'info'); this.terminal.print(`ISO: ${n.toISOString()}`, 'info'); this.terminal.print(`UTC: ${n.toUTCString()}`, 'info'); this.terminal.print(`Unix: ${Math.floor(n.getTime() / 1000)}`, 'info'); } // โ”€โ”€ cal โ”€โ”€ _cmdCal(args) { const now = new Date(); let year = now.getFullYear(); let month = now.getMonth() + 1; if (args.length >= 2) { month = parseInt(args[0]); year = parseInt(args[1]); } else if (args.length === 1) { // If only one arg, could be year (4 digits) or month const v = parseInt(args[0]); if (args[0].length === 4 || v > 12) { year = v; } else { month = v; } } if (isNaN(month) || isNaN(year) || month < 1 || month > 12) { return this.terminal.print('cal: invalid month or year', 'error'); } const monthNames = ['January','February','March','April','May','June', 'July','August','September','October','November','December']; const first = new Date(year, month - 1, 1); const last = new Date(year, month, 0); const daysInMonth = last.getDate(); const startDay = first.getDay(); // 0=Sun const header = ` ${monthNames[month-1]} ${year}`; const weekdays = 'Su Mo Tu We Th Fr Sa'; let cal = header + '\n' + weekdays + '\n'; let line = ' '.repeat(startDay); for (let d = 1; d <= daysInMonth; d++) { line += String(d).padStart(2) + ' '; if ((startDay + d) % 7 === 0 || d === daysInMonth) { cal += line.trimEnd() + '\n'; line = ''; } } this.terminal.print(cal, 'info'); } // โ”€โ”€ yes โ”€โ”€ _cmdYes(args) { const text = args.length > 0 ? args.join(' ') : 'y'; const count = 50; for (let i = 0; i < count; i++) { this.terminal.print(text); } this.terminal.print(`(repeated ${count} times, capped)`, 'text-muted'); } // โ”€โ”€ seq โ”€โ”€ _cmdSeq(args) { if (args.length === 0) return this.terminal.print('Usage: seq or seq ', 'warning'); let first = 1, last; if (args.length === 1) { last = parseInt(args[0]); } else { first = parseInt(args[0]); last = parseInt(args[1]); } if (isNaN(first) || isNaN(last)) return this.terminal.print('seq: non-integer argument', 'error'); const max = 1000; if (last - first > max) return this.terminal.print(`seq: range too large (max ${max})`, 'error'); const output = []; if (first <= last) { for (let i = first; i <= last; i++) output.push(String(i)); } else { for (let i = first; i >= last; i--) output.push(String(i)); } this.terminal.print(output.join('\n'), 'info'); } // โ”€โ”€ base64 โ”€โ”€ _cmdBase64(args) { if (args.length === 0) return this.terminal.print('Usage: base64 [-d] ', 'warning'); if (args[0] === '-d' || args[0] === '--decode') { if (args.length < 2) return this.terminal.print('Usage: base64 -d ', 'warning'); const encoded = args.slice(1).join(' '); try { const decoded = atob(encoded); this.terminal.print(decoded, 'info'); } catch (e) { this.terminal.print('base64: invalid input (not valid base64)', 'error'); } } else { const input = args.join(' '); this.terminal.print(btoa(input), 'info'); } } // โ”€โ”€ printf โ”€โ”€ _cmdPrintf(args) { if (args.length === 0) return this.terminal.print('Usage: printf [args...]', 'warning'); let fmt = args[0]; const vals = args.slice(1); let vi = 0; // Replace format specifiers step by step const result = fmt .replace(/\\n/g, '\n') .replace(/\\t/g, '\t') .replace(/\\\\/g, '\\') .replace(/%s/g, () => vals[vi++] || '') .replace(/%d/g, () => { const v = parseInt(vals[vi++]); return isNaN(v) ? '0' : String(v); }) .replace(/%f/g, () => { const v = parseFloat(vals[vi++]); return isNaN(v) ? '0.000000' : v.toFixed(6); }) .replace(/%[^sdf%]/g, (m) => `(unsupported: ${m})`); this.terminal.print(result, 'info'); } // โ”€โ”€ cut โ”€โ”€ _cmdCut(args) { if (args.length < 2) return this.terminal.print('Usage: cut -f [-d] ', 'warning'); let field = null, delim = '\t', file = ''; for (const a of args) { if (a.startsWith('-f')) field = parseInt(a.substring(2)); else if (a.startsWith('-d')) delim = a.substring(2); else file = a; } if (!field || isNaN(field)) return this.terminal.print('cut: -f flag required', 'error'); if (!file) return this.terminal.print('cut: file argument required', 'error'); const content = this.terminal.fs.readFile(file); if (content === null) return this.terminal.print(`cut: ${file}: No such file`, 'error'); const lines = content.split('\n').filter(l => l); const output = []; for (const line of lines) { const parts = line.split(delim); const idx = field - 1; if (idx >= 0 && idx < parts.length) { output.push(parts[idx]); } } this.terminal.print(output.join('\n'), 'info'); } // โ”€โ”€ uniq โ”€โ”€ _cmdUniq(args) { if (!args[0]) return this.terminal.print('Usage: uniq ', 'warning'); const content = this.terminal.fs.readFile(args[0]); if (content === null) return this.terminal.print(`uniq: ${args[0]}: No such file`, 'error'); const lines = content.split('\n'); let last = null; const output = []; for (const line of lines) { if (line !== last) { output.push(line); last = line; } } this.terminal.print(output.join('\n'), 'info'); } // โ”€โ”€ hostname โ”€โ”€ _cmdHostname() { const ua = navigator.userAgent; const space = 'newos4'; const platform = navigator.platform || 'unknown'; const version = 'WebX11 ' + (window.NEWOS4_VERSION || 'v4.0'); this.terminal.print(`Hostname: ${space}`, 'info'); this.terminal.print(`Platform: ${version} on ${platform}`, 'info'); this.terminal.print(`User Agent: ${ua}`, 'info'); } // โ”€โ”€ whoami โ”€โ”€ _cmdWhoami() { // Try to get from user profile let name = 'darkbot'; try { const prof = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; if (prof && prof._userProfile && prof._userProfile.knownFacts && prof._userProfile.knownFacts.name) { name = prof._userProfile.knownFacts.name; } } catch (e) {} this.terminal.print(name, 'info'); } // โ”€โ”€ basename โ”€โ”€ _cmdBasename(args) { if (!args[0]) return this.terminal.print('Usage: basename ', 'warning'); const parts = args[0].split('/').filter(p => p); const result = parts.pop() || '/'; this.terminal.print(result, 'info'); } // โ”€โ”€ dirname โ”€โ”€ _cmdDirname(args) { if (!args[0]) return this.terminal.print('Usage: dirname ', 'warning'); const parts = args[0].split('/').filter(p => p); if (parts.length <= 1) { this.terminal.print('.', 'info'); return; } parts.pop(); const result = args[0].startsWith('/') ? '/' + parts.join('/') : parts.join('/'); this.terminal.print(result || '.', 'info'); } // โ”€โ”€ sleep โ”€โ”€ _cmdSleep(args) { if (!args[0]) return this.terminal.print('Usage: sleep ', 'warning'); const secs = parseInt(args[0]); if (isNaN(secs) || secs < 1) return this.terminal.print('sleep: invalid duration', 'error'); const max = 30; const actual = Math.min(secs, max); if (actual !== secs) this.terminal.print(`sleep: capped to ${max}s (from ${secs}s)`, 'warning'); this.terminal.print(`Sleeping ${actual}s...`, 'info'); let remaining = actual; const interval = setInterval(() => { remaining--; if (remaining > 0) { this.terminal.print(`${remaining}s...`, 'info'); } else { clearInterval(interval); this.terminal.print('Done', 'success'); } }, 1000); } // โ”€โ”€ expand โ”€โ”€ _cmdExpand(args) { if (!args[0]) return this.terminal.print('Usage: expand ', 'warning'); const content = this.terminal.fs.readFile(args[0]); if (content === null) return this.terminal.print(`expand: ${args[0]}: No such file`, 'error'); const expanded = content.replace(/\t/g, ' '); this.terminal.print(expanded, 'info'); } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new BusyBoxModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/busybox.js'); } /* === end critical ext module bundle === */