diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-14 06:00:14 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-14 06:00:14 +0200 |
| commit | 3dda77c60c19ebe810ac86e35a5c510c737b7b39 (patch) | |
| tree | bfa0ac2c5e96249f7a5010df8bde039e416915bf /modules/core/terminal.js | |
| parent | de713efd619360e29b9bfc4b99d30d2a86b96caf (diff) | |
auto: 2026-06-14_06:00 changes
Diffstat (limited to 'modules/core/terminal.js')
| -rw-r--r-- | modules/core/terminal.js | 111 |
1 files changed, 81 insertions, 30 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 1aad5bb..c416b8b 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -19,7 +19,7 @@ this._registerBuiltIn(); } _registerBuiltIn() { - const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash']; + const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg']; internals.forEach(c => this.registerCommand(c, { category: 'builtin' })); } setLevel(n) { @@ -227,16 +227,18 @@ this._configSetters = {}; this._loadTerminalConfig(); // Register built-in terminal config keys - this.registerConfigItem('terminal-max-lines', 200, (v) => { this.MAX_OUTPUT_LINES = v; }); + this.registerConfigItem('terminal-max-lines', 100, (v) => { this.MAX_OUTPUT_LINES = v; }); this.registerConfigItem('terminal-hist-output', 50, (v) => { this._historyMaxLines = v; }); this.registerConfigItem('terminal-cmd-history', 200, (v) => { /* applied on next save */ }); this.registerConfigItem('terminal-pipe-buffer', 5000, (v) => { this._pipeBufferSize = v; }); + this.registerConfigItem('terminal-sysmsg', true, (v) => { this._showSystemMessages = !!v; }); this._pipeInput = null; this._pipeBufferSize = 5000; this._historyBuffer = []; this._historyMaxLines = 50; this._historySavePending = false; - this.MAX_OUTPUT_LINES = 200; + this.MAX_OUTPUT_LINES = 100; + this._showSystemMessages = true; this._historyCap = 200; // Apply history cap from localStorage on startup (was applied "on next save" before — broken) try { @@ -337,14 +339,7 @@ } else if (e.ctrlKey && (e.key === 'l' || e.key === 'L')) { // Ctrl+L — clear output (standard terminal) e.preventDefault(); - // Preserve welcome/loading indicators — only clear actual output lines - if (this.outputContainer) { - for (const child of [...this.outputContainer.children]) { - if (!child.id || (!child.id.includes('welcome') && !child.id.includes('loading'))) { - child.remove(); - } - } - } + this.clearOutput(); } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C') && this.inputField.value) { // Ctrl+C — cancel current input line (standard terminal) e.preventDefault(); @@ -454,6 +449,13 @@ label.textContent = prefix + (path === '/' ? '$' : path + ' $'); } print(text, type = '') { + if (['error','warning','success','info','text-muted'].includes(type) && !this._showSystemMessages && !type.startsWith('command')) { + // Still log to history buffer so it can be shown again when toggled on, + // but don't create DOM nodes while system messages are hidden. + this._historyBuffer.push({ l: text, t: type }); + if (this._historyBuffer.length > this._historyMaxLines) this._historyBuffer.shift(); + return; + } const line = document.createElement('div'); // Use card layout for typed messages (error/warning/info/success/text-muted), plain for bare text const isTyped = ['error','warning','success'].includes(type); @@ -464,6 +466,13 @@ } else { line.className = 'output-line'; line.textContent = text; + // Plain lines get a lightweight inline copy button + const copyBtn = document.createElement('button'); + copyBtn.className = 'line-copy-btn'; + copyBtn.textContent = 'Copy'; + copyBtn.setAttribute('onclick', 'webx11.copyLineToClipboard(this)'); + copyBtn.setAttribute('aria-label', 'Copy line'); + line.appendChild(copyBtn); } this.outputContainer.appendChild(line); this._historyBuffer.push({ l: text, t: type }); @@ -600,17 +609,15 @@ const nd = btn.nextElementSibling; if (nd) text = nd.innerText || nd.textContent; } + this._copyText(text, btn); + } + copyLineToClipboard(btn) { + const line = btn.parentElement; + const text = line ? line.childNodes[0].textContent : ''; + this._copyText(text, btn); + } + _copyText(text, btn=null) { if (!text) return; - btn.textContent = 'Copied!'; - setTimeout(function(){ btn.textContent = 'Copy'; }, 2000); - // Use navigator.clipboard on secure contexts (iOS 14+, modern browsers). - // This is the preferred API — NOTHING blocks it on HTTPS pages. - if (navigator.clipboard) { - navigator.clipboard.writeText(text).catch(function(){}); - } - // Also try textarea+execCommand synchronously for older browsers. - // DO NOT wrap in setTimeout — iOS Safari requires execCommand to run - // in the same microtask as the user's click event. try { var ta = document.createElement('textarea'); ta.value = text; @@ -621,7 +628,11 @@ ta.setSelectionRange(0, text.length); document.execCommand('copy'); document.body.removeChild(ta); - } catch(e) {} + if (btn) { btn.textContent = 'Copied!'; setTimeout(function(){ btn.textContent = 'Copy'; }, 2000); } + this.print('📋 Copied to clipboard', 'success'); + } catch (e) { + this.print('Copy failed: ' + e.message, 'error'); + } } printClickable(items) { const line = document.createElement('div'); @@ -760,10 +771,9 @@ } } _saveHistory() { - if (this._historySavePending) return; - this._historySavePending = true; - setTimeout(() => { - this._historySavePending = false; + if (this._historySaveTimer) clearTimeout(this._historySaveTimer); + this._historySaveTimer = setTimeout(() => { + this._historySaveTimer = null; try { const slice = this._historyBuffer.slice(-this._historyMaxLines); localStorage.setItem('webx11_terminal_output', JSON.stringify(slice)); @@ -785,6 +795,33 @@ container.removeChild(container.firstChild); } } + toggleExpand() { + const container = document.getElementById('terminal-container'); + if (!container) return; + container.classList.toggle('terminal-expanded'); + const on = container.classList.contains('terminal-expanded'); + this.print(on ? 'Terminal expanded' : 'Terminal compact', 'text-muted'); + } + toggleSystemMessages(arg) { + let desired; + if (arg === 'on') desired = true; + else if (arg === 'off') desired = false; + else desired = !this._showSystemMessages; + this._showSystemMessages = desired; + this._saveTerminalConfig(); + this.print('System messages ' + (desired ? 'on' : 'off'), 'text-muted'); + } + clearOutput() { + if (this.outputContainer) { + this.outputContainer.innerHTML = ''; + } + this._historyBuffer = []; + if (this._historySaveTimer) { + clearTimeout(this._historySaveTimer); + this._historySaveTimer = null; + } + try { localStorage.removeItem('webx11_terminal_output'); } catch (e) {} + } markDirty() { this._dirty = true; } @@ -1238,6 +1275,7 @@ if (parsed['terminal-hist-output'] !== undefined) this._historyMaxLines = parsed['terminal-hist-output']; if (parsed['terminal-cmd-history'] !== undefined) this._historyCap = parsed['terminal-cmd-history']; if (parsed['terminal-pipe-buffer'] !== undefined) this._pipeBufferSize = parsed['terminal-pipe-buffer']; + this._trimOutput(); } catch (e) {} } async _loadExtModules(filterKeys) { @@ -1347,7 +1385,9 @@ return trimmed; } async dispatchCommand(cmd, args) { - if (cmd === 'clear') { this.outputContainer.innerHTML = ''; this._historyBuffer = []; localStorage.removeItem('webx11_terminal_output'); return; } + if (cmd === 'clear') { this.clearOutput(); return; } + if (cmd === 'expand') { this.toggleExpand(); return; } + if (cmd === 'sysmsg') { this.toggleSystemMessages(args[0]); return; } if (cmd === 'startup-modules') { this.openStartupModules(); return; } if (cmd === 'font-size' || cmd === 'setfont') { this.setFontSize(args[0]); return; } if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; } @@ -1356,24 +1396,31 @@ if (cmd === 'config') { if (args.length === 0) { this.print('=== Terminal Config ===', 'info'); - this.print(`terminal-max-lines: ${this.MAX_OUTPUT_LINES} (default: 200)`, ''); + this.print(`terminal-max-lines: ${this.MAX_OUTPUT_LINES} (default: 100)`, ''); this.print(`terminal-hist-output: ${this._historyMaxLines} (default: 50)`, ''); this.print(`terminal-cmd-history: ${this._historyCap} (default: 200)`, ''); this.print(`terminal-pipe-buffer: ${this._pipeBufferSize} (default: 5000)`, ''); + this.print(`terminal-sysmsg: ${this._showSystemMessages ? 'on' : 'off'} (default: on)`, ''); this.print('Usage: config <key> <value>', 'info'); - this.print('Keys: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer', 'info'); + this.print('Keys: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer, terminal-sysmsg', 'info'); return; } const key = args[0]; const val = args[1]; if (val === undefined) { this.print('Usage: config <key> <value>', 'warning'); return; } const n = parseInt(val); + if (key === 'terminal-sysmsg') { + this._showSystemMessages = val !== 'off'; + this._saveTerminalConfig(); + this.print(`config ${key} set to ${this._showSystemMessages ? 'on' : 'off'}`, 'success'); + return; + } if (isNaN(n) || n < 1) { this.print('Value must be a positive number', 'warning'); return; } if (key === 'terminal-max-lines') { this.MAX_OUTPUT_LINES = n; } else if (key === 'terminal-hist-output') { this._historyMaxLines = n; } else if (key === 'terminal-cmd-history') { this._historyCap = n; } else if (key === 'terminal-pipe-buffer') { this._pipeBufferSize = n; } - else { this.print(`Unknown key: ${key}. Valid: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer`, 'warning'); return; } + else { this.print(`Unknown key: ${key}. Valid: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer, terminal-sysmsg`, 'warning'); return; } this._saveTerminalConfig(); this.print(`config ${key} set to ${val}`, 'success'); return; @@ -2407,6 +2454,8 @@ printMemoryStats() { const pm = typeof performance !== 'undefined' && performance.memory; let heapInfo = ''; + let domNodes = 0; + try { domNodes = document.getElementsByTagName('*').length; } catch (e) {} if (pm) { const used = (pm.usedJSHeapSize / 1048576).toFixed(1); const total = (pm.totalJSHeapSize / 1048576).toFixed(1); @@ -2423,9 +2472,11 @@ const output = [ `📊 Memory Stats`, ` ${heapInfo}`, + ` DOM nodes: ${domNodes}`, ` Terminal lines in DOM: ${this.outputContainer.children.length}`, ` History buffer: ${this._historyBuffer.length} entries`, ` MAX_OUTPUT_LINES: ${this.MAX_OUTPUT_LINES}`, + ` History-backed output lines: ${this._historyBuffer.length}`, ` Commands in history: ${this.history.length}` ].join('\n'); this.print(output, 'info'); |