aboutsummaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-14 06:00:14 +0200
committerAgando <roduccomm@gmail.com>2026-06-14 06:00:14 +0200
commit3dda77c60c19ebe810ac86e35a5c510c737b7b39 (patch)
treebfa0ac2c5e96249f7a5010df8bde039e416915bf /modules/core/terminal.js
parentde713efd619360e29b9bfc4b99d30d2a86b96caf (diff)
auto: 2026-06-14_06:00 changes
Diffstat (limited to 'modules/core/terminal.js')
-rw-r--r--modules/core/terminal.js111
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');