// ── Fake terminal: click the Environment card's icon ────────── (function () { const glyph = document.querySelector('.skill-card-icon .fa-terminal'); if (!glyph) return; const icon = glyph.closest('.skill-card-icon'); const HOST = 'gade.gg'; const KERNEL = '6.6-goat'; const T0 = performance.now(); // Current user: su can change it (root's password leaked in auth.log), // exit pops back. Root passes every permission check and may write. const USERS = { root: ['root'], nikolaj: ['home', 'nikolaj'], guest: ['home', 'guest'], pepsi: ['home', 'pepsi'], }; const PASSWORDS = { root: 'Pepsi2022' }; let user = 'guest'; let home = USERS.guest; const userStack = []; let pendingSu = null; // user being su'd to while the password prompt is up // Filesystem: dirs are { children }, files are strings — or functions // returning a string, for live content (.bash_history). locked dirs // deny everyone but root and their owner. Only root may write. const dir = (children) => ({ children }); const locked = (owner, children) => ({ children, locked: owner }); const DENIED = { denied: true }; // a path tried to pass through a locked dir const isFile = (n) => typeof n === 'string' || typeof n === 'function'; const fileBody = (n) => (typeof n === 'function' ? n() : n); const allowed = (n) => !n.locked || user === 'root' || user === n.locked; const GOAT = ` %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%% %%%% %%%%%%%%%%% %%% %%%%%%%%%%%%%%% %% %%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%% %%%%%%%%%%%%%%% `; const BIN = '\u007fELF��������'; const FS = dir({ bin: dir({ cat: BIN, echo: BIN, gsh: BIN, ls: BIN, nano: BIN, rm: BIN, sh: BIN }), etc: dir({ hostname: 'gade.gg\n', hosts: '127.0.0.1 localhost\n' + '10.0.0.1 bulbasaur\n' + '10.0.0.6 charizard\n' + '10.0.0.29 nidoran-f\n' + '10.0.0.32 nidoran-m\n' + '10.0.0.58 growlithe\n' + '10.0.0.63 abra\n' + '10.0.0.64 kadabra\n' + '10.0.0.133 eevee\n' + '10.0.0.150 mewtwo\n' + '10.0.0.151 mew\n', motd: "Welcome to gade.gg.\nType 'help' for commands.\n", passwd: 'root:x:0:0:root:/root:/bin/sh\n' + 'nikolaj:x:1000:1000:Nikolaj Gade:/home/nikolaj:/bin/fish\n' + 'guest:x:1001:1001:guest:/home/guest:/bin/gsh\n' + 'pepsi:x:1002:1002:Pepsi Max Lime Gade:/home/pepsi:/usr/bin/meow\n', }), home: dir({ guest: dir({ 'readme.txt': 'Nothing here yet. Have a look around.\n', '.bash_history': () => (history.length ? history.join('\n') + '\n' : ''), '.bashrc': "# ~/.bashrc\nPS1='\\u@\\h:\\w\\$ '\nalias sl=ls\n", }), nikolaj: locked('nikolaj', { 'array-props.pdf': '%PDF-1.7\n%����\n', }), pepsi: dir({ '.plan': 'nap.\n' + 'nap.\n' + 'knock something off the table.\n' + 'nap.\n', }), }), proc: dir({ uptime: () => { const s = (performance.now() - T0) / 1000; return s.toFixed(2) + ' ' + (s * 4).toFixed(2) + '\n'; }, version: 'Linux version ' + KERNEL + ' (rats@gade.gg) (gcc (GCC) 14.2.1) #1 SMP PREEMPT\n', }), root: locked('root', { goat: GOAT, '.bash_history': 'rm -rf /\n', }), usr: dir({ bin: dir({ meow: BIN }), share: dir({ centvrion: dir({ 'salve.cent': '// Hello, world\nDIC("SALVE MVNDE")\n' }), }), }), var: dir({ log: dir({ 'auth.log': 'Aug 9 03:12:44 gade sshd[812]: Failed password for root from 10.0.0.150 port 51434 ssh2\n' + 'Aug 9 03:12:51 gade sshd[815]: Failed password for invalid user Pepsi2022 from 10.0.0.150 port 51438 ssh2\n' + 'Aug 9 03:13:02 gade sshd[819]: Accepted password for root from 10.0.0.150 port 51442 ssh2\n', }), }), }); let cwd = [...USERS.guest]; function resolvePath(path) { if (path === '~') path = '/' + home.join('/'); else if (path.startsWith('~/')) path = '/' + home.join('/') + path.slice(1); const segs = path.startsWith('/') ? [] : [...cwd]; for (const part of path.split('/')) { if (part === '' || part === '.') continue; if (part === '..') segs.pop(); else segs.push(part); } return segs; } function nodeAt(segs) { let node = FS; for (const s of segs) { if (!allowed(node)) return DENIED; if (!node.children || !Object.prototype.hasOwnProperty.call(node.children, s)) return null; node = node.children[s]; } return node; } function displayPath(segs) { const p = '/' + segs.join('/'); const h = '/' + home.join('/'); if (p === h) return '~'; if (p.startsWith(h + '/')) return '~' + p.slice(h.length); return p; } const promptText = () => user + '@' + HOST + ':' + displayPath(cwd) + (user === 'root' ? '#' : '$'); let dialog = null; let screen = null; let out = null; let input = null; let promptEl = null; let termTitle = null; function becomeUser(u) { user = u; home = USERS[u]; // The quiet flag a future achievements system reads if (u === 'root') { try { localStorage.setItem('termRoot', '1'); } catch (e) {} if (window.achv) window.achv.unlock('hacker'); } promptEl.textContent = promptText(); termTitle.textContent = u + '@' + HOST; } // Real history: ↑ recalls it, ~/.bash_history reads it, localStorage // keeps it. Seeded so a first visit finds a previous guest's session. const HIST_SEED = ['ls', 'cd /', 'ls', 'cat etc/motd', 'cd']; const HIST_MAX = 200; let history = HIST_SEED; try { history = JSON.parse(localStorage.getItem('termHistory')) || HIST_SEED; } catch (e) {} if (!Array.isArray(history)) history = HIST_SEED; history = history.slice(-HIST_MAX).map(String); let hIdx = history.length; let draft = ''; function saveHistory() { try { localStorage.setItem('termHistory', JSON.stringify(history)); } catch (e) {} } // parts: [{ text, cls? }] — one scrollback line function line(parts) { const el = document.createElement('div'); el.className = 'term-line'; for (const part of parts) { if (part.cls) { const span = document.createElement('span'); span.className = part.cls; span.textContent = part.text; el.appendChild(span); } else { el.appendChild(document.createTextNode(part.text)); } } out.appendChild(el); } const print = (text) => line([{ text }]); // hidden commands work but stay out of help and tab completion — // finding them is the point const ALIASES = { sl: 'ls' }; // honoring ~/.bashrc const COMMANDS = { cat: { usage: 'cat ', desc: 'print a file', run(args) { for (const t of args) { const node = nodeAt(resolvePath(t)); if (node === null) print('cat: ' + t + ': No such file or directory'); else if (node === DENIED) print('cat: ' + t + ': Permission denied'); else if (isFile(node)) { const body = fileBody(node).replace(/\n$/, ''); if (body) print(body); } else print('cat: ' + t + ': Is a directory'); } } }, cd: { usage: 'cd [dir]', desc: 'change directory', run(args) { const target = args[0] || '~'; const segs = resolvePath(target); const node = nodeAt(segs); if (node === null) print('cd: no such file or directory: ' + target); else if (node === DENIED || !allowed(node)) print('cd: permission denied: ' + target); else if (isFile(node)) print('cd: not a directory: ' + target); else { cwd = segs; promptEl.textContent = promptText(); } } }, clear: { usage: 'clear', desc: 'clear the screen', run() { out.textContent = ''; } }, echo: { usage: 'echo [text]', desc: 'print text', run(args) { print(args.join(' ')); } }, exit: { usage: 'exit', desc: 'close the terminal', run() { if (userStack.length) becomeUser(userStack.pop()); else dialog.close(); } }, help: { usage: 'help', desc: 'list commands', run() { const names = Object.keys(COMMANDS).filter((n) => !COMMANDS[n].hidden).sort(); const pad = Math.max(...names.map((n) => COMMANDS[n].usage.length)) + 2; names.forEach((n) => print(' ' + COMMANDS[n].usage.padEnd(pad) + COMMANDS[n].desc)); } }, history: { hidden: true, usage: 'history', desc: '', run() { history.forEach((h, i) => print(String(i + 1).padStart(5) + ' ' + h)); } }, ls: { usage: 'ls [-a] [dir]', desc: 'list directory contents', run(args) { const all = args.some((a) => /^-[a-zA-Z]*a/.test(a)); const paths = args.filter((a) => !a.startsWith('-')); const target = paths[0] || '.'; const node = nodeAt(resolvePath(target)); if (node === null) { print("ls: cannot access '" + target + "': No such file or directory"); return; } if (node === DENIED || !allowed(node)) { print("ls: cannot open directory '" + target + "': Permission denied"); return; } if (isFile(node)) { print(target); return; } let names = Object.keys(node.children).sort(); names = all ? ['.', '..', ...names] : names.filter((n) => !n.startsWith('.')); if (!names.length) return; const parts = []; names.forEach((n, i) => { if (i) parts.push({ text: ' ' }); const child = n === '.' || n === '..' ? FS : node.children[n]; parts.push(isFile(child) ? { text: n } : { text: n, cls: 'term-dir' }); }); line(parts); } }, meow: { hidden: true, usage: 'meow', desc: '', run() { print('meow'); } }, nano: { usage: 'nano [file]', desc: 'edit a file', run(args) { const target = args[0]; let name = 'New Buffer'; let content = ''; let segs = null; if (target) { segs = resolvePath(target); const node = nodeAt(segs); if (node === DENIED) { print('nano: ' + target + ': Permission denied'); return; } if (node !== null && !isFile(node)) { print('nano: ' + target + ': Is a directory'); return; } if (node !== null) content = fileBody(node); name = segs[segs.length - 1] || target; } openNano(name, content, segs); } }, pwd: { usage: 'pwd', desc: 'print working directory', run() { print('/' + cwd.join('/')); } }, rm: { usage: 'rm [-rf] ', desc: 'remove files', run(args) { const recursive = args.some((a) => /^-[a-zA-Z]*r/i.test(a) || a === '--recursive'); const targets = args.filter((a) => !a.startsWith('-')); if (!targets.length) { print('rm: missing operand'); return; } for (const t of targets) { const segs = resolvePath(t); // The one write the filesystem allows: rm -rf / is the real // delete-website flow, confirmation dialog and all if (recursive && segs.length === 0) { const dlg = document.getElementById('deleteDialog'); if (dlg) { dlg.showModal(); // Chromium: once a nested modal has closed, the input under it // still gets key events but no longer inserts text — only a // fresh input element types again dlg.addEventListener('close', () => { const fresh = input.cloneNode(); input.replaceWith(fresh); input = fresh; wireInput(fresh); fresh.focus(); }, { once: true }); return; } } const node = nodeAt(segs); if (node === null) print("rm: cannot remove '" + t + "': No such file or directory"); else if (node === DENIED) print("rm: cannot remove '" + t + "': Permission denied"); else if (!isFile(node) && !recursive) print("rm: cannot remove '" + t + "': Is a directory"); else if (user !== 'root') print("rm: cannot remove '" + t + "': Permission denied"); else { const parent = nodeAt(segs.slice(0, -1)); delete parent.children[segs[segs.length - 1]]; } } } }, roll: { usage: 'roll [NdS+M]', desc: 'roll dice', run(args) { const spec = (args[0] || 'd20').toLowerCase(); const m = /^(\d*)d(\d+)([+-]\d+)?$/.exec(spec); if (!m) { print('roll: usage: roll [NdS+M], e.g. roll 2d6+1'); return; } const n = Math.min(parseInt(m[1] || '1', 10) || 1, 100); const sides = parseInt(m[2], 10); const mod = parseInt(m[3] || '0', 10); const rolls = []; for (let i = 0; i < n; i++) rolls.push(1 + Math.floor(Math.random() * sides)); const total = rolls.reduce((a, r) => a + r, 0) + mod; if (n === 1 && !mod) { print(spec + ': ' + total); return; } let breakdown = rolls.join(' + '); if (mod) breakdown += mod > 0 ? ' + ' + mod : ' - ' + -mod; print(spec + ': ' + breakdown + ' = ' + total); } }, su: { hidden: true, usage: 'su [user]', desc: '', run(args) { const target = args.filter((a) => !a.startsWith('-'))[0] || 'root'; if (!(target in USERS)) { print('su: user ' + target + ' does not exist'); return; } if (user === 'root') { userStack.push(user); becomeUser(target); return; } pendingSu = target; promptEl.textContent = 'Password:'; input.type = 'password'; } }, sudo: { hidden: true, usage: 'sudo', desc: '', run(args) { if (user !== 'root') { print(user + ' is not in the sudoers file. This incident will be reported.'); return; } if (!args.length) return; const cmd = COMMANDS[ALIASES[args[0]] || args[0]]; if (cmd) cmd.run(args.slice(1)); else print('sudo: ' + args[0] + ': command not found'); } }, uname: { hidden: true, usage: 'uname', desc: '', run(args) { if (args.includes('-a')) print('Linux ' + HOST + ' ' + KERNEL + ' #1 SMP PREEMPT x86_64 GNU/Linux'); else if (args.includes('-r')) print(KERNEL); else print('Linux'); } }, whoami: { usage: 'whoami', desc: 'print user name', run() { print(user); } }, }; function runLine(raw) { line([{ text: promptText() + ' ', cls: 'term-ps1' }, { text: raw }]); const trimmed = raw.trim(); if (trimmed) { if (trimmed !== history[history.length - 1]) { history.push(trimmed); if (history.length > HIST_MAX) history.shift(); saveHistory(); } const tokens = trimmed.split(/\s+/); const cmd = COMMANDS[ALIASES[tokens[0]] || tokens[0]]; if (cmd) cmd.run(tokens.slice(1)); else print('gsh: ' + tokens[0] + ': command not found'); } hIdx = history.length; draft = ''; screen.scrollTop = screen.scrollHeight; } // Tab: complete command names (first word) or paths (arguments) to the // longest shared prefix; a stuck tab with several matches lists them function complete() { const v = input.value; const token = /(\S*)$/.exec(v)[1]; const before = v.slice(0, v.length - token.length); let base = ''; let candidates; if (before.trim() === '') { candidates = Object.keys(COMMANDS) .filter((c) => !COMMANDS[c].hidden && c.startsWith(token)) .map((c) => c + ' '); } else { const slash = token.lastIndexOf('/'); base = slash >= 0 ? token.slice(0, slash + 1) : ''; const prefix = token.slice(slash + 1); const node = nodeAt(resolvePath(base || '.')); if (!node || node === DENIED || !allowed(node) || isFile(node)) return; candidates = Object.keys(node.children) .filter((n) => n.startsWith(prefix) && (prefix.startsWith('.') || !n.startsWith('.'))) .map((n) => n + (isFile(node.children[n]) ? ' ' : '/')); } if (!candidates.length) return; let lcp = candidates[0]; for (const c of candidates) { while (!c.startsWith(lcp)) lcp = lcp.slice(0, -1); } if (candidates.length === 1) { input.value = before + base + candidates[0]; } else if (lcp && base + lcp !== token) { input.value = before + base + lcp; } else { line(candidates.map((c, i) => ({ text: (i ? ' ' : '') + c.trimEnd() }))); screen.scrollTop = screen.scrollHeight; } } // ── nano: the one installed editor. Editing works; saving is another // matter (the filesystem is read-only, and it says so like nano would) let nano = null; let nanoTitleName = null; let nanoTitleMod = null; let nanoEdit = null; let nanoStatus = null; let nanoKeys = null; let nanoName = ''; let nanoSegs = null; let nanoOriginal = ''; let nanoPrompting = false; const NANO_EDIT_KEYS = [['^X', 'Exit'], ['^O', 'Write Out']]; function setNanoKeys(pairs) { nanoKeys.textContent = ''; pairs.forEach(([key, label]) => { const k = document.createElement('span'); k.className = 'nano-key'; k.textContent = key; nanoKeys.appendChild(k); nanoKeys.appendChild(document.createTextNode(' ' + label + ' ')); }); } function setNanoStatus(msg) { nanoStatus.textContent = ''; if (msg) { const s = document.createElement('span'); s.textContent = msg; nanoStatus.appendChild(s); } } function updateNanoTitle() { nanoTitleName.textContent = nanoName; nanoTitleMod.textContent = nanoEdit.value === nanoOriginal ? '' : 'Modified'; } function buildNano() { nano = document.createElement('div'); nano.className = 'nano'; const title = document.createElement('div'); title.className = 'nano-bar nano-title'; const version = document.createElement('span'); version.textContent = 'GNU nano 8.1'; nanoTitleName = document.createElement('span'); nanoTitleMod = document.createElement('span'); title.append(version, nanoTitleName, nanoTitleMod); nanoEdit = document.createElement('textarea'); nanoEdit.className = 'nano-edit'; nanoEdit.spellcheck = false; nanoEdit.setAttribute('aria-label', 'Editor'); nanoStatus = document.createElement('div'); nanoStatus.className = 'nano-status'; nanoKeys = document.createElement('div'); nanoKeys.className = 'nano-keys'; nano.append(title, nanoEdit, nanoStatus, nanoKeys); nanoEdit.addEventListener('keydown', nanoKeydown); nanoEdit.addEventListener('input', updateNanoTitle); } function openNano(name, content, segs) { if (!nano) buildNano(); nanoName = name; nanoSegs = segs; nanoOriginal = content; nanoEdit.value = content; nanoPrompting = false; setNanoStatus(''); setNanoKeys(NANO_EDIT_KEYS); updateNanoTitle(); screen.classList.add('nano-open'); screen.appendChild(nano); nanoEdit.focus(); nanoEdit.setSelectionRange(0, 0); } function closeNano() { nanoPrompting = false; screen.classList.remove('nano-open'); nano.remove(); input.focus(); screen.scrollTop = screen.scrollHeight; } function nanoAskExit() { nanoPrompting = true; setNanoStatus('Save modified buffer?'); setNanoKeys([['Y', 'Yes'], ['N', 'No'], ['^C', 'Cancel']]); } function nanoEndPrompt(msg) { nanoPrompting = false; setNanoStatus(msg); setNanoKeys(NANO_EDIT_KEYS); } // Writing works for root and no one else; sets its own status line function nanoSave() { if (user !== 'root') { setNanoStatus('[ Error writing ' + nanoName + ': Permission denied ]'); return false; } if (!nanoSegs || !nanoSegs.length) { setNanoStatus('[ Cancelled ]'); return false; } const parent = nodeAt(nanoSegs.slice(0, -1)); if (!parent || parent === DENIED || isFile(parent)) { setNanoStatus('[ Directory does not exist ]'); return false; } parent.children[nanoSegs[nanoSegs.length - 1]] = nanoEdit.value; nanoOriginal = nanoEdit.value; updateNanoTitle(); const n = nanoEdit.value ? nanoEdit.value.replace(/\n$/, '').split('\n').length : 0; setNanoStatus('[ Wrote ' + n + ' line' + (n === 1 ? '' : 's') + ' ]'); return true; } function nanoKeydown(e) { if (nanoPrompting) { e.preventDefault(); const k = e.key.toLowerCase(); if (k === 'y') { nanoPrompting = false; setNanoKeys(NANO_EDIT_KEYS); if (nanoSave()) closeNano(); } else if (k === 'n') closeNano(); else if ((k === 'c' && e.ctrlKey) || e.key === 'Escape') nanoEndPrompt('[ Cancelled ]'); return; } if (e.ctrlKey && !e.altKey && !e.metaKey) { const k = e.key.toLowerCase(); if (k === 'x') { e.preventDefault(); if (nanoEdit.value !== nanoOriginal) nanoAskExit(); else closeNano(); } else if (k === 'o' || k === 's') { e.preventDefault(); nanoSave(); } return; // other ctrl combos (copy, paste, undo) keep their defaults } if (e.key === 'Escape') { e.preventDefault(); if (nanoEdit.value !== nanoOriginal) nanoAskExit(); else closeNano(); } else if (e.key === 'Tab') { e.preventDefault(); nanoEdit.setRangeText('\t', nanoEdit.selectionStart, nanoEdit.selectionEnd, 'end'); updateNanoTitle(); } } function build() { dialog = document.createElement('dialog'); dialog.className = 'term-dialog'; dialog.setAttribute('aria-label', 'Terminal'); const bar = document.createElement('div'); bar.className = 'term-titlebar'; termTitle = document.createElement('span'); termTitle.className = 'term-title'; termTitle.textContent = user + '@' + HOST; bar.appendChild(termTitle); screen = document.createElement('div'); screen.className = 'term-screen'; out = document.createElement('div'); out.className = 'term-out'; out.setAttribute('role', 'log'); const row = document.createElement('div'); row.className = 'term-inputrow'; promptEl = document.createElement('span'); promptEl.className = 'term-prompt'; promptEl.setAttribute('aria-hidden', 'true'); promptEl.textContent = promptText(); input = document.createElement('input'); input.className = 'term-input'; input.type = 'text'; input.autocapitalize = 'off'; input.autocomplete = 'off'; input.spellcheck = false; input.setAttribute('aria-label', 'Command input'); row.append(promptEl, input); screen.append(out, row); dialog.append(bar, screen); document.body.appendChild(dialog); const motd = nodeAt(resolvePath('/etc/motd')); if (typeof motd === 'string') print(motd.replace(/\n$/, '')); wireInput(input); // Click focuses the input, but not when selecting scrollback text screen.addEventListener('click', () => { const sel = window.getSelection(); if (!sel || sel.isCollapsed) input.focus(); }); // Click on the backdrop closes, like the delete dialog dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.close(); }); // While nano is up, Escape belongs to nano, not the dialog dialog.addEventListener('cancel', (e) => { if (screen.classList.contains('nano-open')) e.preventDefault(); }); } function wireInput(el) { el.addEventListener('keydown', (e) => { if (e.ctrlKey && !e.altKey && !e.metaKey) { const k = e.key.toLowerCase(); if (k === 'c') { e.preventDefault(); if (pendingSu) { pendingSu = null; input.type = 'text'; line([{ text: 'Password:^C' }]); promptEl.textContent = promptText(); } else { line([{ text: promptText() + ' ', cls: 'term-ps1' }, { text: input.value + '^C' }]); } input.value = ''; hIdx = history.length; draft = ''; screen.scrollTop = screen.scrollHeight; } else if (k === 'd') { e.preventDefault(); if (pendingSu) { pendingSu = null; input.type = 'text'; input.value = ''; line([{ text: 'Password:' }]); promptEl.textContent = promptText(); } else if (input.value === '') COMMANDS.exit.run(); } else if (k === 'l') { e.preventDefault(); out.textContent = ''; } return; // unhandled ctrl combos (paste, …) keep their defaults } if (e.key === 'Enter') { // preventDefault kills the trailing keypress: if a command opens a // modal (rm -rf /), the keypress would land on its autofocused // button and click it before the dialog is ever seen e.preventDefault(); const raw = input.value; input.value = ''; if (pendingSu) { // password attempts stay out of scrollback and history const target = pendingSu; pendingSu = null; input.type = 'text'; line([{ text: 'Password:' }]); if (raw === PASSWORDS[target]) { userStack.push(user); becomeUser(target); } else { print('su: Authentication failure'); promptEl.textContent = promptText(); } screen.scrollTop = screen.scrollHeight; return; } runLine(raw); } else if (e.key === 'ArrowUp') { e.preventDefault(); if (pendingSu) return; if (hIdx > 0) { if (hIdx === history.length) draft = input.value; hIdx--; input.value = history[hIdx]; input.setSelectionRange(input.value.length, input.value.length); } } else if (e.key === 'ArrowDown') { e.preventDefault(); if (pendingSu) return; if (hIdx < history.length) { hIdx++; input.value = hIdx === history.length ? draft : history[hIdx]; input.setSelectionRange(input.value.length, input.value.length); } } else if (e.key === 'Tab') { e.preventDefault(); if (!pendingSu) complete(); } }); } function open() { if (!dialog) build(); // lazy: no trace in the DOM until first opened if (dialog.open) return; dialog.showModal(); if (screen.classList.contains('nano-open')) nanoEdit.focus(); else input.focus(); screen.scrollTop = screen.scrollHeight; } icon.addEventListener('click', open); })();