diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
| commit | ccc7d9956559d2195e20345e7a794662b52cd403 (patch) | |
| tree | 757354fbfec5c75b4577b2cb620d5ab1582e19b1 /modules/core/terminal.js | |
| parent | b538577cd0a4292e4cd4ca1c9f9829fc7e133caa (diff) | |
auto: 2026-06-06_06:00 changes
Diffstat (limited to 'modules/core/terminal.js')
| -rw-r--r-- | modules/core/terminal.js | 350 |
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; |