aboutsummaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/terminal.js')
-rw-r--r--modules/core/terminal.js350
1 files changed, 339 insertions, 11 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index d916ae6..be57c05 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -11,10 +11,15 @@
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history']);
+ // Load persisted overrides from localStorage
+ try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
+ try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
+ try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
+ try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
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','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor'];
+ 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'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -73,8 +78,25 @@
}
return best ? { cmd: best, dist: bestDist } : null;
}
+ _parseArgs(input) {
+ const args = [];
+ let current = '';
+ let inDouble = false;
+ let inSingle = false;
+ for (const ch of input) {
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
+ if (ch === ' ' && !inDouble && !inSingle) {
+ if (current) { args.push(current); current = ''; }
+ } else {
+ current += ch;
+ }
+ }
+ if (current) args.push(current);
+ return args;
+ }
_classify(raw) {
- const parts = raw.trim().split(/\s+/);
+ const parts = this._parseArgs(raw.trim());
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
// Internal commands always allowed at terminal level, but security still classifies
@@ -199,12 +221,23 @@
this.moduleManager = new ModuleManager(this);
this.security = new WebX11SecurityManager(this, parseInt(localStorage.getItem('webx11_security_level')) || 1);
this.ollamaManager = new OllamaManager(this);
+ // Config registry — modules can register keys for the unified `config` command
+ this._configRegistry = {};
+ this._configDefaults = {};
+ 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-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._pipeInput = null;
this._pipeBufferSize = 5000;
this._historyBuffer = [];
this._historyMaxLines = 50;
this._historySavePending = false;
this.MAX_OUTPUT_LINES = 200;
+ this._historyCap = 200;
this._aiBusy = false;
this._dirty = false;
this.history = JSON.parse(localStorage.getItem('webx11_command_history') || '[]');
@@ -221,6 +254,8 @@
// Auto-restore session
this._autoRestoreSession();
this._restoreFontSize();
+ // Restore saved theme on boot
+ this._restoreTheme();
// Init auto-save timer — 60s after boot, then every 60s
setTimeout(() => this._initAutoSave(), 30000);
// Init sound toggle button text
@@ -231,6 +266,10 @@
setTimeout(() => this.inputField.focus(), 100);
// First-run welcome check — fires after ext modules are loaded
setTimeout(() => this._checkFirstRun(), 2000);
+ // Init all registered modules (safe now — constructor is done)
+ this._initRegisteredModules();
+ // Apply config overrides after everything is initialized
+ this._applyTerminalConfig();
}
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
@@ -244,7 +283,14 @@
}
}
const cmd = this.inputField.value;
- this.history.push(cmd);
+ // Dedup: don't push if same as last entry
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
+ this.history.push(cmd);
+ }
+ // Trim to max 200 entries
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
this.historyIndex = this.history.length;
this._saveCommandHistory();
this.executeCommand(cmd);
@@ -739,7 +785,7 @@
var rootClone;
try {
const json = JSON.stringify(this.fs.root);
- if (json.length > 5242880) { // 5MB cap
+ if (json.length > 52428800) { // 50MB cap
rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
this.print('⚠ Auto-save skipped (VFS too large)', 'warning');
return;
@@ -761,7 +807,7 @@
terminalLines: this._historyBuffer.slice(-50)
};
const snapshotJson = JSON.stringify(snapshot);
- if (snapshotJson.length > 5242880) {
+ if (snapshotJson.length > 52428800) {
this.print('⚠ Auto-save snapshot too large, skipping', 'warning');
return;
}
@@ -851,9 +897,14 @@
.map(e => e.type === 'dir' ? prefix + e.name + '/' : prefix + e.name);
}
showAutocomplete() {
+ if (!this.moduleManager || !this.security) return;
const input = this.inputField.value;
const trimmed = input.trim();
- const commands = this.moduleManager.getAllCommands();
+ const commands = this.moduleManager.getAllCommands() || {};
+ // Merge built-in internals into autocomplete (they're handled via if-chain, not module commands)
+ for (const c of this.security.commandMeta.keys()) {
+ if (!(c in commands)) commands[c] = 'builtin';
+ }
this.suggestions = [];
this.selectedSuggestion = -1;
this._isCdAutocomplete = false;
@@ -1020,7 +1071,6 @@
this._safeRegister('WebX11AIAssistantModule', 'AI assistant');
this._safeRegister('WebFetcherModule', 'web fetcher');
this._safeRegister('WebXAIAliasModule', 'AI alias');
- this._safeRegister('SkillsModule', 'skills');
this._safeRegister('SecretVault', 'secret vault');
this.reloadPersistedModules();
this._safeRegister('ChatStudioModule', 'chat studio');
@@ -1035,6 +1085,15 @@
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
}
+ _initRegisteredModules() {
+ for (const [name, mod] of Object.entries(this.moduleManager.modules)) {
+ if (typeof mod.init === 'function') {
+ try { mod.init(this); } catch (e) {
+ console.warn(`[Init] ${name} failed: ${e.message}`);
+ }
+ }
+ }
+ }
_safeRegister(className, label) {
let cls;
try {
@@ -1050,6 +1109,56 @@
try { this.moduleManager.registerModule(new cls()); }
catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); }
}
+ // ── Config Registry ──
+ registerConfigItem(key, defaultValue, setterFn) {
+ this._configRegistry[key] = { key, defaultValue };
+ this._configDefaults[key] = defaultValue;
+ this._configSetters[key] = setterFn;
+ // Apply persisted override if present
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ if (parsed[key] !== undefined && parsed[key] !== defaultValue) {
+ setterFn(parsed[key]);
+ }
+ } catch (e) {}
+ }
+ }
+ _loadTerminalConfig() {
+ try {
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (!saved) return;
+ const parsed = JSON.parse(saved);
+ // Apply registered config items from persistence
+ for (const [key, val] of Object.entries(parsed)) {
+ if (this._configSetters[key] && val !== this._configDefaults[key]) {
+ this._configSetters[key](val);
+ }
+ }
+ } catch (e) {}
+ }
+ _saveTerminalConfig() {
+ const values = {};
+ for (const key of Object.keys(this._configRegistry)) {
+ const actual = this[key] !== undefined ? this[key] : this._configDefaults[key];
+ if (actual !== this._configDefaults[key]) {
+ values[key] = actual;
+ }
+ }
+ localStorage.setItem('webx11_terminal_config', JSON.stringify(values));
+ }
+ _applyTerminalConfig() {
+ try {
+ const saved = localStorage.getItem('webx11_terminal_config');
+ if (!saved) return;
+ const parsed = JSON.parse(saved);
+ if (parsed['terminal-max-lines'] !== undefined) this.MAX_OUTPUT_LINES = parsed['terminal-max-lines'];
+ 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'];
+ } catch (e) {}
+ }
async _loadExtModules(filterKeys) {
const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
@@ -1083,6 +1192,16 @@
async executeCommand(rawInput) {
if (!rawInput.trim()) return;
this.markDirty();
+ // Push to command history (dedup + trim)
+ const cmd = rawInput.trim();
+ if (this.history.length === 0 || this.history[this.history.length - 1] !== cmd) {
+ this.history.push(cmd);
+ }
+ if (this.history.length > this._historyCap) {
+ this.history = this.history.slice(-this._historyCap);
+ }
+ this.historyIndex = this.history.length;
+ this._saveCommandHistory();
this.printUserPrompt(rawInput);
const pipeParts = this.splitPipe(rawInput.trim());
if (pipeParts.length > 1) {
@@ -1108,7 +1227,7 @@
let pipeInput = null;
for (let i = 0; i < pipeParts.length; i++) {
const cmdText = pipeParts[i];
- const parts = cmdText.split(/\s+/);
+ const parts = this.security._parseArgs(cmdText);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
this._pipeInput = pipeInput;
@@ -1139,9 +1258,40 @@
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ 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-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('Usage: config <key> <value>', 'info');
+ this.print('Keys: terminal-max-lines, terminal-hist-output, terminal-cmd-history, terminal-pipe-buffer', '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 (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; }
+ this._saveTerminalConfig();
+ this.print(`config ${key} set to ${val}`, 'success');
+ return;
+ }
if (cmd === 'security') {
this.print(`Security level: ${this.security.getLevel()} (${this.security.getLevelName()})`, 'info');
this.print(`Blocked commands: ${this.security.blockedCount}`, 'info');
+ this.print(`Destructive set: ${[...this.security.destructiveCmds].sort().join(', ')}`, '');
+ this.print(`System set: ${[...this.security.systemCmds].sort().join(', ')}`, '');
+ this.print(`Suspicious flags: ${[...this.security.suspiciousFlags].sort().join(', ')}`, '');
+ this.print(`Whitelist: ${[...this.security.whitelist].sort().join(', ')}`, '');
+ const all = this.security._collectAllCommands();
+ this.print(`Registered commands: ${all.length}`, '');
return;
}
if (cmd === 'security-level') {
@@ -1151,14 +1301,160 @@
localStorage.setItem('webx11_security_level', lv);
return;
}
+ if (cmd === 'security-destructive') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-destructive add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Destructive commands:', 'info');
+ for (const c of this.security.destructiveCmds) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-destructive add|remove <cmd>', 'warning'); return; }
+ if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
+ else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
+ else { this.print('Action must be: add, remove, or list', 'warning'); return; }
+ localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-system') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-system add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('System commands:', 'info');
+ for (const c of this.security.systemCmds) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-system add|remove <cmd>', 'warning'); return; }
+ if (action === 'add') { this.security.systemCmds.add(args[1]); }
+ else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
+ else { this.print('Action must be: add, remove, or list', 'warning'); return; }
+ localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-whitelist') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-whitelist add|remove|list [cmd]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Whitelist commands:', 'info');
+ for (const c of this.security.whitelist) this.print(` ${c}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-whitelist add|remove <cmd>', 'warning'); return; }
+ if (action === 'add') { this.security.whitelist.add(args[1]); }
+ else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
+ else { this.print('Action must be: add, remove, or list', 'warning'); return; }
+ localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
+ return;
+ }
+ if (cmd === 'security-flag') {
+ const action = args[0];
+ if (!action) { this.print('Usage: security-flag add|remove|list [flag]', 'warning'); return; }
+ if (action === 'list') {
+ this.print('Suspicious flags:', 'info');
+ for (const f of this.security.suspiciousFlags) this.print(` ${f}`, '');
+ return;
+ }
+ if (!args[1]) { this.print('Usage: security-flag add|remove <flag>', 'warning'); return; }
+ const flag = args[1].startsWith('--') ? args[1] : '--' + args[1];
+ if (action === 'add') { this.security.suspiciousFlags.add(flag); }
+ else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
+ else { this.print('Action must be: add, remove, or list', 'warning'); return; }
+ localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
+ return;
+ }
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
+ if (cmd === 'vfs' && args[0] === 'reset') {
+ if (args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ return;
+ }
+ this.fs.reset();
+ this.print('VFS reset — all files deleted.', 'success');
+ return;
+ }
+ if (cmd === 'vfs' && args[0] === 'clean') {
+ let count = 0;
+ const sysDirs = ['_snapshots', '_sessions', '.trash'];
+ for (const dir of sysDirs) {
+ if (this.fs.root.content && this.fs.root.content[dir]) {
+ delete this.fs.root.content[dir];
+ count++;
+ }
+ }
+ if (count > 0) {
+ this.fs.saveToStorage();
+ this.print('🧹 Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success');
+ } else {
+ this.print('Nothing to clean — system dirs already empty.', 'info');
+ }
+ return;
+ }
+ if (cmd === 'vfs-clean') {
+ let count = 0;
+ const sysDirs = ['_snapshots', '_sessions', '.trash'];
+ for (const dir of sysDirs) {
+ if (this.fs.root.content && this.fs.root.content[dir]) {
+ delete this.fs.root.content[dir];
+ count++;
+ }
+ }
+ if (count > 0) {
+ this.fs.saveToStorage();
+ this.print('🧹 Cleaned ' + count + ' system dirs: ' + sysDirs.join(', '), 'success');
+ } else {
+ this.print('Nothing to clean — system dirs already empty.', 'info');
+ }
+ return;
+ }
+ if (cmd === 'empty-trash') {
+ if (args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ return;
+ }
+ if (this.fs.emptyTrash()) {
+ this.print('Trash emptied.', 'success');
+ } else {
+ this.print('Trash is already empty.', 'info');
+ }
+ return;
+ }
+ if (cmd === 'history') {
+ const search = args.join(' ').toLowerCase();
+ const entries = this.history;
+ if (!entries.length) { this.print('No command history.', 'info'); return; }
+ if (search) {
+ const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search));
+ if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; }
+ this.print(`=== History (matching: ${search}) ===`, 'info');
+ filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`));
+ } else {
+ this.print(`=== Command History (${entries.length} entries) ===`, 'info');
+ const start = Math.max(0, entries.length - 50);
+ for (let i = start; i < entries.length; i++) {
+ this.print(` ${i + 1} ${entries[i]}`);
+ }
+ if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`);
+ }
+ return;
+ }
for (const modName in this.moduleManager.modules) {
const mod = this.moduleManager.modules[modName];
if (mod.commands && mod.commands[cmd]) {
try { await mod.commands[cmd](args);
// Notify tutorial module for quest tracking
if (this.tutorialModule && cmd !== 'tutorial') this.tutorialModule.onCommandExecuted(cmd, args);
+ // Memory graph: record file access for read/file commands
+ if (cmd === 'cat' || cmd === 'view_module' || cmd === 'edit_module') {
+ try {
+ const _mg = this.moduleManager.modules['MemoryGraphModule'];
+ if (_mg && args[0]) _mg.recordFileAccess(args[0]);
+ } catch (_e) {}
+ }
return; }
catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
}
@@ -1336,8 +1632,7 @@
openSkillsManager() {
const modal = document.getElementById('skills-manager');
// Ensure /skills/ directory exists
- const skillsMod = this.moduleManager.modules['SkillsModule'];
- if (skillsMod && skillsMod.ensureSkillsDir) skillsMod.ensureSkillsDir();
+ this.fs.mkdirRecursive('/skills');
this.refreshSkillsList();
modal.style.display = 'flex';
}
@@ -1453,12 +1748,16 @@
if (!confirm(`Delete skill "${name}"?`)) return;
const fs = this.fs;
fs.mkdirRecursive('/skills');
- fs.writeFile(`/skills/${name}.md`, '');
+ // Actually remove from VFS tree
+ if (fs.root && fs.root.content && fs.root.content['skills'] && fs.root.content['skills'].content) {
+ delete fs.root.content['skills'].content[`${name}.md`];
+ }
const index = fs.fsReadJSON('/skills/index.json') || {};
delete index[name];
fs.fsWriteJSON('/skills/index.json', index);
this.print(`Skill '${name}' deleted`, 'success');
this.refreshSkillsList();
+ fs.saveToStorage();
}
createSkillFromUI() {
const name = document.getElementById('skill-name-input').value.trim().replace(/\s+/g, '-').toLowerCase();
@@ -1890,6 +2189,35 @@
}
} catch (e) {}
}
+ _restoreTheme() {
+ try {
+ const savedTheme = localStorage.getItem('webx11_active_theme');
+ if (savedTheme) {
+ const themesRaw = localStorage.getItem('webx11_themes');
+ const defaults = {
+ 'dark': { '--bg-color': '#000000', '--bg-secondary': '#0a0a0a', '--bg-tertiary': '#111111', '--text-color': '#00ff00', '--text-muted': '#888888', '--accent-color': '#00aa00', '--border-color': '#005500' },
+ 'light': { '--bg-color': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-tertiary': '#eeeeee', '--text-color': '#222222', '--text-muted': '#888888', '--accent-color': '#0066cc', '--border-color': '#cccccc' },
+ 'amber': { '--bg-color': '#1a0f00', '--bg-secondary': '#2a1a00', '--bg-tertiary': '#332200', '--text-color': '#ffaa00', '--text-muted': '#886600', '--accent-color': '#ff8800', '--border-color': '#663300' },
+ 'cyber': { '--bg-color': '#000a14', '--bg-secondary': '#001a2e', '--bg-tertiary': '#002240', '--text-color': '#00ffff', '--text-muted': '#0088aa', '--accent-color': '#ff00ff', '--border-color': '#004466' },
+ 'ocean': { '--bg-color': '#0a1628', '--bg-secondary': '#0f2240', '--bg-tertiary': '#142e54', '--text-color': '#88ddff', '--text-muted': '#5599bb', '--accent-color': '#00aaff', '--border-color': '#1a4470' },
+ 'forest': { '--bg-color': '#0a1a0e', '--bg-secondary': '#0f2814', '--bg-tertiary': '#14361c', '--text-color': '#88dd88', '--text-muted': '#559955', '--accent-color': '#33cc33', '--border-color': '#1a4a22' },
+ 'monochrome': { '--bg-color': '#0a0a0a', '--bg-secondary': '#181818', '--bg-tertiary': '#222222', '--text-color': '#cccccc', '--text-muted': '#666666', '--accent-color': '#999999', '--border-color': '#333333' },
+ 'sunset': { '--bg-color': '#1a0a14', '--bg-secondary': '#2a1220', '--bg-tertiary': '#3a1a2c', '--text-color': '#ffbb99', '--text-muted': '#cc7755', '--accent-color': '#ff6644', '--border-color': '#552233' },
+ 'midnight': { '--bg-color': '#050510', '--bg-secondary': '#0a0a20', '--bg-tertiary': '#101030', '--text-color': '#aaaaee', '--text-muted': '#555588', '--accent-color': '#6666ff', '--border-color': '#1a1a3a' }
+ };
+ if (themesRaw) {
+ const themes = JSON.parse(themesRaw);
+ Object.assign(defaults, themes);
+ }
+ const t = defaults[savedTheme];
+ if (t) {
+ for (const [k, v] of Object.entries(t)) {
+ document.documentElement.style.setProperty(k, v);
+ }
+ }
+ }
+ } catch (e) { console.warn('[Theme] restore error:', e); }
+ }
// ── First-run welcome ──
_checkFirstRun() {
if (localStorage.getItem('webx11_tutorial_complete') || localStorage.getItem('webx11_tutorial_skipped')) return;