<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Evgenii Zobnin</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --green:      #39ff14;
      --green-glow: rgba(57, 255, 20, 0.55);
      --green-dim:  #b0ffb0;
      --white:      #e8e8e8;
      --red:        #ff5555;
      --bg:         #0b0b0b;
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    html, body {
      min-height: 100%;
      background: var(--bg);
      color: var(--green);
      font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Courier New', monospace;
      font-size: clamp(13px, 1.8vw, 15px);
      line-height: 1.75;
    }

    body::before {
      content: '';
      position: fixed; inset: 0;
      background: repeating-linear-gradient(
        180deg, transparent, transparent 3px,
        rgba(0,0,0,0.06) 3px, rgba(0,0,0,0.06) 4px
      );
      pointer-events: none; z-index: 1000;
    }

    body::after {
      content: '';
      position: fixed; inset: 0;
      background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.45) 100%);
      pointer-events: none; z-index: 999;
    }

    #screen {
      max-width: 860px;
      margin: 0 auto;
      padding: 32px 28px 80px;
    }

    .ln {
      display: block;
      white-space: pre-wrap;
      word-break: break-word;
      min-height: 1em;
    }

    .ln-prompt .p-user  { color: var(--green); font-weight: 700; text-shadow: 0 0 8px var(--green-glow); }
    .ln-prompt .p-sep   { color: #555; }
    .ln-prompt .p-path  { color: #6af; }
    .ln-prompt .p-dollar{ color: var(--green); font-weight: 700; }
    .ln-prompt .p-cmd   { color: var(--white); }

    .ln-out  { color: var(--green-dim); }
    .ln-err  { color: var(--red); }
    .ln-gap  { min-height: 0.6em; }

    .ln-out a { color: #6af; text-underline-offset: 3px; }
    .ln-out a:hover { color: #adf; }

    /* ls colours */
    .c-dir  { color: #6af; font-weight: bold; }
    .c-dot  { color: #555; }
    .c-file { color: var(--green-dim); }

    @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }

    .cursor {
      display: inline-block;
      width: .6em; height: 0.9em;
      background: var(--green);
      vertical-align: baseline;
      animation: blink 1.1s step-end infinite;
      box-shadow: 0 0 6px var(--green-glow);
    }

    #input-row {
      display: none;
      align-items: baseline;
      flex-wrap: wrap;
      cursor: text;
    }

    #input-prompt { white-space: pre; flex-shrink: 0; }
    #input-display { color: var(--white); white-space: pre; }

    #live-input {
      position: absolute; left: -9999px;
      width: 1px; height: 1px; opacity: 0;
    }

    @media (max-width: 600px) {
      #screen { padding: 16px 14px 60px; }
    }
  </style>
</head>
<body>
<div id="screen">
  <div id="output-area"></div>
  <div id="input-row">
    <span id="input-prompt" class="ln-prompt"></span>
    <span id="input-display" class="p-cmd"></span>
    <span class="cursor" id="ibeam"></span>
    <input id="live-input" type="text"
           autocomplete="off" autocorrect="off"
           autocapitalize="off" spellcheck="false">
  </div>
</div>

<script>
'use strict';

// ── Config ────────────────────────────────────────────────────────────────────
const USER  = 'evgenii';
const HOST  = 'arch';
const HOME  = '/home/evgenii';
const BIRTH = new Date('1985-12-18T00:00:00');

let cwd        = HOME;
let hist       = [];
let histIdx    = -1;
let activeProc = null;   // { abort() } — прерываемый async-процесс

// ── Virtual Filesystem ────────────────────────────────────────────────────────
// 'd' = каталог,  ['f', content] = файл
const VFS = {
  '/':           'd',
  '/bin':        'd',
  '/etc':        'd',
  '/etc/hostname':   ['f', 'arch'],
  '/etc/os-release': ['f',
    'NAME="Arch Linux"\nPRETTY_NAME="Arch Linux"\nID=arch\nBUILD_ID=rolling\n' +
    'HOME_URL="https://archlinux.org/"'],
  '/etc/passwd': ['f',
    `root:x:0:0:root:/root:/bin/bash\n${USER}:x:1000:1000:Evgenii Zobnin:/home/${USER}:/bin/bash`],
  '/etc/fstab':  ['f', [
    '# <device>   <dir>   <type>  <options>   <dump> <pass>',
    '/dev/sda1    /       ext4    defaults    0      1',
    '/dev/sda2    /boot   vfat    defaults    0      2',
    'tmpfs        /tmp    tmpfs   defaults    0      0',
  ].join('\n')],
  '/home':                'd',
  '/home/evgenii':        'd',
  '/home/evgenii/whoami.md':     ['f', ''],   // загружается с диска
  '/home/evgenii/experience.md': ['f', ''],   // загружается с диска
  '/home/evgenii/contacts.md':   ['f', ''],   // загружается с диска
  '/home/evgenii/.bashrc': ['f',
    "# ~/.bashrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\nalias ll='ls -la'\nalias la='ls -A'\nalias grep='grep --color=auto'"],
  '/home/evgenii/.bash_history': ['f',
    'ls\ncat whoami.md\ncd projects\nls\nping google.com -c 4\nuname -a\nneofetch'],
  '/home/evgenii/projects':                          'd',
  '/home/evgenii/projects/aio-launcher':             'd',
  '/home/evgenii/projects/aio-launcher/README.md':   ['f',
    '# AIO Launcher\n\nA highly customizable Android home screen replacement.\n' +
    'Written in Kotlin.\n\n' +
    'https://play.google.com/store/apps/details?id=ru.execbit.aiolauncher'],
  '/home/evgenii/projects/linux-book':               'd',
  '/home/evgenii/projects/linux-book/README.md':     ['f',
    '# Linux: A Practical Guide\n\nA book about Linux for developers and power users.\n' +
    'Covers shell scripting, system administration, networking.'],
  '/proc':              'd',
  '/proc/version':      ['f', 'Linux version 6.12.0-arch1-1 (gcc version 14.2.1) #1 SMP PREEMPT_DYNAMIC'],
  '/proc/cpuinfo':      ['f', [
    'processor\t: 0', 'vendor_id\t: GenuineIntel',
    'model name\t: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
    'cpu MHz\t\t: 2600.000', 'cache size\t: 12288 KB', 'bogomips\t: 5199.98', '',
    'processor\t: 1', 'vendor_id\t: GenuineIntel',
    'model name\t: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz',
    'cpu MHz\t\t: 2600.000', 'cache size\t: 12288 KB', 'bogomips\t: 5199.98',
  ].join('\n')],
  '/proc/meminfo': ['f', [
    'MemTotal:       16384000 kB', 'MemFree:         8294400 kB',
    'MemAvailable:   10485760 kB', 'Buffers:          524288 kB',
    'Cached:          2097152 kB', 'SwapTotal:       8388608 kB', 'SwapFree:  8388608 kB',
  ].join('\n')],
  '/tmp':          'd',
  '/var':          'd',
  '/var/log':      'd',
  '/var/log/pacman.log': ['f', [
    '[2024-11-01 09:23] [ALPM] installed linux (6.12.0.arch1-1)',
    '[2024-11-15 14:45] [ALPM] upgraded neovim (0.10.1 -> 0.10.2)',
    '[2024-12-01 11:00] [ALPM] upgraded firefox (131.0 -> 132.0)',
    '[2025-01-10 18:30] [ALPM] upgraded git (2.46.0 -> 2.47.0)',
    '[2025-02-14 08:15] [ALPM] upgraded linux (6.12.0 -> 6.12.1)',
  ].join('\n')],
};

const CORE = new Set([
  '/home/evgenii/whoami.md',
  '/home/evgenii/experience.md',
  '/home/evgenii/contacts.md',
]);

// ── Path utilities ────────────────────────────────────────────────────────────
function normPath(p) {
  const out = [];
  for (const s of p.split('/').filter(Boolean)) {
    if (s === '.') continue;
    if (s === '..') { if (out.length) out.pop(); }
    else out.push(s);
  }
  return '/' + out.join('/');
}

function resolvePath(p) {
  if (!p || p === '~')      return HOME;
  if (p.startsWith('~/'))   return normPath(HOME + '/' + p.slice(2));
  if (p.startsWith('/'))    return normPath(p);
  return normPath(cwd + '/' + p);
}

const isDir  = p => VFS[p] === 'd';
const isFile = p => Array.isArray(VFS[p]);
const fread  = p => Array.isArray(VFS[p]) ? VFS[p][1] : '';

function listDir(dir) {
  const prefix = dir === '/' ? '/' : dir + '/';
  const seen = new Set();
  const out  = [];
  for (const k of Object.keys(VFS)) {
    if (k === dir || !k.startsWith(prefix)) continue;
    const name = k.slice(prefix.length).split('/')[0];
    if (!seen.has(name)) {
      seen.add(name);
      const fp = (dir === '/' ? '' : dir) + '/' + name;
      out.push({ name, d: isDir(fp) });
    }
  }
  return out.sort((a, b) => {
    if (a.d !== b.d) return a.d ? -1 : 1;
    return a.name.localeCompare(b.name);
  });
}

// ── Prompt ────────────────────────────────────────────────────────────────────
function promptPath() {
  if (cwd === HOME)               return '~';
  if (cwd.startsWith(HOME + '/')) return '~' + cwd.slice(HOME.length);
  return cwd;
}

function promptHTML() {
  return `<span class="p-user">${USER}</span><span class="p-sep">@</span>` +
         `<span class="p-user">${HOST}</span><span class="p-sep">:</span>` +
         `<span class="p-path">${promptPath()}</span>` +
         `<span class="p-dollar">$&nbsp;</span>`;
}

// ── DOM helpers ───────────────────────────────────────────────────────────────
const outputArea   = document.getElementById('output-area');
const inputRow     = document.getElementById('input-row');
const liveInput    = document.getElementById('live-input');
const inputPrompt  = document.getElementById('input-prompt');
const inputDisplay = document.getElementById('input-display');

const sleep    = ms => new Promise(r => setTimeout(r, ms));
const scrollEnd = () => window.scrollTo(0, document.body.scrollHeight);

function esc(s) {
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function linkify(s) {
  return esc(s).replace(/(https?:\/\/[^\s<>"]+)/g,
    '<a href="$1" target="_blank" rel="noopener">$1</a>');
}

function appendRaw(html, cls = '') {
  const el = document.createElement('span');
  el.className = 'ln' + (cls ? ' ' + cls : '');
  el.innerHTML = html;
  outputArea.appendChild(el);
  scrollEnd();
  return el;
}

function appendGap()  { appendRaw('', 'ln-gap'); }
function appendText(text, cls = 'ln-out') {
  text.split('\n').forEach(line => appendRaw(linkify(line), cls));
}

function appendResult(r) {
  if (!r) return;
  if (r.out  !== undefined) appendText(r.out);
  if (r.html !== undefined) r.html.split('\n').forEach(l => appendRaw(l, 'ln-out'));
  if (r.err  !== undefined) appendText(r.err, 'ln-err');
}

// ── Tab completion ────────────────────────────────────────────────────────────
const ALL_CMDS = [
  'cat','cd','clear','contacts','date','df','echo','env','experience',
  'free','grep','head','help','history','hostname','id','ifconfig','ip',
  'ls','man','mkdir','neofetch','ping','printenv','ps','pwd','rm',
  'tail','touch','uname','uptime','wc','which','whoami',
];

function commonPfx(strs) {
  if (!strs.length) return '';
  let p = strs[0];
  for (const s of strs.slice(1)) while (!s.startsWith(p)) p = p.slice(0, -1);
  return p;
}

function tabComplete(val) {
  const parts = val.split(' ');
  const last  = parts[parts.length - 1];

  if (parts.length === 1) {
    const m = ALL_CMDS.filter(c => c.startsWith(last));
    if (m.length === 1) return m[0];
    if (m.length > 1)   { appendText(m.join('  ')); return commonPfx(m); }
    return val;
  }

  const si = last.lastIndexOf('/');
  let dir, pfx;
  if      (si === -1) { dir = cwd;               pfx = last; }
  else if (si === 0)  { dir = '/';               pfx = last.slice(1); }
  else                { dir = resolvePath(last.slice(0, si)); pfx = last.slice(si + 1); }

  const entries = listDir(dir).filter(e => e.name.startsWith(pfx));
  const base = si === -1 ? '' : last.slice(0, si + 1);

  if (entries.length === 1) {
    const completed = base + entries[0].name + (entries[0].d ? '/' : '');
    return parts.slice(0, -1).concat(completed).join(' ');
  }
  if (entries.length > 1) {
    appendText(entries.map(e => e.name + (e.d ? '/' : '')).join('  '));
    return parts.slice(0, -1).concat(base + commonPfx(entries.map(e => e.name))).join(' ');
  }
  return val;
}

// ── Commands ──────────────────────────────────────────────────────────────────
function runCommand(cmd, args) {
  switch (cmd) {

    // ── navigation ──────────────────────────────────────────────────────────
    case 'pwd': return { out: cwd };

    case 'cd': {
      const t = resolvePath(args[0]);
      if (!VFS[t])   return { err: `bash: cd: ${args[0] || HOME}: No such file or directory` };
      if (!isDir(t)) return { err: `bash: cd: ${args[0]}: Not a directory` };
      cwd = t;
      return null;
    }

    case 'ls': {
      const longF = args.some(a => /^-.*l/.test(a));
      const allF  = args.some(a => /^-.*a/.test(a));
      const pathArg = args.find(a => !a.startsWith('-'));
      const target  = pathArg ? resolvePath(pathArg) : cwd;

      if (!VFS[target]) return { err: `ls: cannot access '${pathArg}': No such file or directory` };
      if (isFile(target)) return { html: `<span class="c-file">${esc(pathArg)}</span>` };

      let entries = listDir(target);
      if (allF) entries = [{ name: '.', d: true }, { name: '..', d: true }, ...entries];

      const dateStr = 'Feb 14';

      if (longF) {
        const lines = entries.map(e => {
          const p  = e.d ? 'drwxr-xr-x' : '-rw-r--r--';
          const sz = e.d ? '   4096' : '   1024';
          const cl = e.d ? 'c-dir' : e.name.startsWith('.') ? 'c-dot' : 'c-file';
          return `${p} 1 ${USER} ${USER} ${sz} ${dateStr} <span class="${cl}">${esc(e.name)}</span>`;
        });
        return { html: lines.join('\n') };
      }

      const items = entries.map(e => {
        if (e.d)              return `<span class="c-dir">${esc(e.name)}/</span>`;
        if (e.name[0] === '.') return `<span class="c-dot">${esc(e.name)}</span>`;
        return `<span class="c-file">${esc(e.name)}</span>`;
      });
      return { html: items.join('  ') };
    }

    // ── file ops ─────────────────────────────────────────────────────────────
    case 'cat': {
      if (!args.length) return { err: 'cat: missing operand' };
      const parts = [];
      for (const a of args) {
        const p = resolvePath(a);
        if (!VFS[p])    return { err: `cat: ${a}: No such file or directory` };
        if (isDir(p))   return { err: `cat: ${a}: Is a directory` };
        parts.push(fread(p));
      }
      return { out: parts.join('\n') };
    }

    case 'head': {
      const n = headTailN(args, 10);
      const f = args.find(a => !a.startsWith('-'));
      if (!f) return { err: 'head: missing operand' };
      const p = resolvePath(f);
      if (!VFS[p])  return { err: `head: ${f}: No such file or directory` };
      if (isDir(p)) return { err: `head: ${f}: Is a directory` };
      return { out: fread(p).split('\n').slice(0, n).join('\n') };
    }

    case 'tail': {
      const n = headTailN(args, 10);
      const f = args.find(a => !a.startsWith('-'));
      if (!f) return { err: 'tail: missing operand' };
      const p = resolvePath(f);
      if (!VFS[p])  return { err: `tail: ${f}: No such file or directory` };
      if (isDir(p)) return { err: `tail: ${f}: Is a directory` };
      return { out: fread(p).split('\n').slice(-n).join('\n') };
    }

    case 'grep': {
      const iFlag = args.includes('-i');
      const nFlag = args.includes('-n');
      const flags = args.filter(a => a.startsWith('-'));
      const rest  = args.filter(a => !a.startsWith('-'));
      if (rest.length < 2) return { err: 'Usage: grep [options] pattern file' };
      const [pat, fname] = rest;
      const p = resolvePath(fname);
      if (!VFS[p])  return { err: `grep: ${fname}: No such file or directory` };
      if (isDir(p)) return { err: `grep: ${fname}: Is a directory` };
      let re;
      try { re = new RegExp(pat, iFlag ? 'i' : ''); }
      catch { return { err: 'grep: invalid regular expression' }; }
      const lines = fread(p).split('\n');
      const hits  = lines
        .map((l, i) => ({ l, i: i + 1 }))
        .filter(x => re.test(x.l));
      if (!hits.length) return null;
      return { out: hits.map(x => nFlag ? `${x.i}:${x.l}` : x.l).join('\n') };
    }

    case 'wc': {
      const lFlag = args.includes('-l');
      const wFlag = args.includes('-w');
      const f = args.find(a => !a.startsWith('-'));
      if (!f) return { err: 'wc: missing operand' };
      const p = resolvePath(f);
      if (!VFS[p])  return { err: `wc: ${f}: No such file or directory` };
      const c = fread(p);
      const L = c.split('\n').length;
      const W = c.split(/\s+/).filter(Boolean).length;
      const B = c.length;
      if (lFlag) return { out: `${String(L).padStart(7)} ${f}` };
      if (wFlag) return { out: `${String(W).padStart(7)} ${f}` };
      return { out: `${String(L).padStart(7)} ${String(W).padStart(7)} ${String(B).padStart(7)} ${f}` };
    }

    case 'mkdir': {
      if (!args.length) return { err: 'mkdir: missing operand' };
      for (const a of args) {
        const p = resolvePath(a);
        if (VFS[p]) return { err: `mkdir: cannot create directory '${a}': File exists` };
        VFS[p] = 'd';
      }
      return null;
    }

    case 'touch': {
      if (!args.length) return { err: 'touch: missing file operand' };
      args.forEach(a => { const p = resolvePath(a); if (!VFS[p]) VFS[p] = ['f', '']; });
      return null;
    }

    case 'rm': {
      if (!args.length) return { err: 'rm: missing operand' };
      const recursive = args.some(a => /^-.*r/i.test(a));
      const files = args.filter(a => !a.startsWith('-'));
      for (const a of files) {
        const p = resolvePath(a);
        if (!VFS[p])             return { err: `rm: cannot remove '${a}': No such file or directory` };
        if (isDir(p) && !recursive) return { err: `rm: cannot remove '${a}': Is a directory` };
        if (CORE.has(p))         return { err: `rm: cannot remove '${a}': Permission denied` };
        Object.keys(VFS).filter(k => k === p || k.startsWith(p + '/')).forEach(k => delete VFS[k]);
      }
      return null;
    }

    // ── system info ──────────────────────────────────────────────────────────
    case 'whoami':
      return { out: fread('/home/evgenii/whoami.md') };

    case 'experience':
      return { out: fread('/home/evgenii/experience.md') };

    case 'contacts':
      return { out: fread('/home/evgenii/contacts.md') };

    case 'uname': {
      if (args.includes('-a') || args.includes('--all'))
        return { out: `Linux ${HOST} 6.12.0-arch1-1 #1 SMP PREEMPT_DYNAMIC x86_64 Evgenii Zobnin` };
      if (args.includes('-r')) return { out: '6.12.0-arch1-1' };
      if (args.includes('-m')) return { out: 'x86_64' };
      if (args.includes('-n')) return { out: HOST };
      return { out: 'Evgenii Zobnin' };
    }

    case 'hostname':
      return { out: args.includes('-I') ? '192.168.1.42' : HOST };

    case 'uptime': {
      const now  = new Date();
      const days = Math.floor((now - BIRTH) / 86400000);
      const hh   = String(now.getHours()).padStart(2, '0');
      const mm   = String(now.getMinutes()).padStart(2, '0');
      return { out: ` ${hh}:${mm}  up ${days.toLocaleString('en')} days,  load average: 0.42, 0.37, 0.41` };
    }

    case 'date':  return { out: new Date().toString() };

    case 'id':
      return { out: `uid=1000(${USER}) gid=1000(${USER}) groups=1000(${USER}),998(wheel),10(audio),987(video)` };

    case 'ps': {
      const full = args.some(a => a.includes('a'));
      if (full) return { out: [
        'USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND',
        'root           1  0.0  0.0  19116  1024 ?        Ss   Dec18   0:01 /sbin/init',
        `${USER}      142  0.0  0.1   8936  4096 pts/0    Ss   Dec18   0:00 /bin/bash`,
        `${USER}      420  0.1  0.3  45200 51200 pts/0    S    09:42   2:13 vim experience.md`,
        `${USER}     2048  0.0  0.0   9888  4096 pts/0    R+   ${new Date().toTimeString().slice(0,5)}   0:00 ps ${args.join(' ')}`,
      ].join('\n') };
      return { out: [
        '  PID TTY          TIME CMD',
        '  142 pts/0    00:00:00 bash',
        ' 2048 pts/0    00:00:00 ps',
      ].join('\n') };
    }

    case 'df': {
      const h = args.includes('-h') || args.some(a => a.includes('h'));
      return { out: h
        ? 'Filesystem      Size  Used Avail Use% Mounted on\n' +
          '/dev/sda1       256G   42G  214G  17% /\n' +
          'tmpfs           7.8G     0  7.8G   0% /dev/shm\n' +
          '/dev/sda2       512M   45M  467M   9% /boot'
        : 'Filesystem     1K-blocks      Used Available Use% Mounted on\n' +
          '/dev/sda1      268435456  44040192 224395264  17% /\n' +
          'tmpfs            8192000         0   8192000   0% /dev/shm\n' +
          '/dev/sda2         524288     46080    478208   9% /boot'
      };
    }

    case 'free': {
      const h = args.includes('-h');
      return { out: h
        ? '               total        used        free      shared  buff/cache   available\n' +
          'Mem:            15Gi       4.2Gi       8.1Gi       324Mi       2.9Gi        10Gi\n' +
          'Swap:          8.0Gi          0B       8.0Gi'
        : '               total        used        free      shared  buff/cache   available\n' +
          'Mem:        16384000     4300000     8294400      332000     3789600    11100000\n' +
          'Swap:        8388608           0     8388608'
      };
    }

    case 'ip': {
      if (!args[0] || args[0] === 'addr' || args[0] === 'a' || args[0] === 'address')
        return { out: [
          '1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN',
          '    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00',
          '    inet 127.0.0.1/8 scope host lo',
          '2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq state UP',
          '    link/ether de:ad:be:ef:ca:fe brd ff:ff:ff:ff:ff:ff',
          '    inet 192.168.1.42/24 brd 192.168.1.255 scope global eth0',
        ].join('\n') };
      if (args[0] === 'route' || args[0] === 'r')
        return { out: 'default via 192.168.1.1 dev eth0\n192.168.1.0/24 dev eth0 proto kernel src 192.168.1.42' };
      return { err: `ip: unknown command '${args[0]}'` };
    }

    case 'ifconfig':
      return { out: [
        'eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500',
        '        inet 192.168.1.42  netmask 255.255.255.0  broadcast 192.168.1.255',
        '        ether de:ad:be:ef:ca:fe  txqueuelen 1000  (Ethernet)',
        '',
        'lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536',
        '        inet 127.0.0.1  netmask 255.0.0.0',
      ].join('\n') };

    // ── misc ─────────────────────────────────────────────────────────────────
    case 'which': {
      if (!args.length) return { err: 'which: missing argument' };
      return ALL_CMDS.includes(args[0])
        ? { out: `/bin/${args[0]}` }
        : { err: `which: no ${args[0]} in (/usr/local/bin:/usr/bin:/bin)` };
    }

    case 'env':
    case 'printenv':
      return { out: [
        `USER=${USER}`, `HOME=${HOME}`, `SHELL=/bin/bash`,
        `TERM=xterm-256color`, `LANG=en_US.UTF-8`,
        `PATH=/home/${USER}/.local/bin:/usr/local/bin:/usr/bin:/bin`,
        `PWD=${cwd}`, `HOSTNAME=${HOST}`,
      ].join('\n') };

    case 'history':
      return hist.length
        ? { out: hist.slice().reverse().map((c, i) => `  ${String(i+1).padStart(4)}  ${c}`).join('\n') }
        : { out: '' };

    case 'echo':
      return { out: args.join(' ') };

    case 'man': {
      if (!args.length) return { err: 'What manual page do you want?' };
      const pages = {
        ls:     'LS(1)\n\nSYNOPSIS\n    ls [-la] [FILE]\n\nOPTIONS\n    -l  long listing\n    -a  include hidden files',
        cd:     'CD(1)\n\nSYNOPSIS\n    cd [DIR]\n\nDESCRIPTION\n    Change directory. Without argument, go home.',
        ping:   'PING(8)\n\nSYNOPSIS\n    ping [-c count] destination\n\nOPTIONS\n    -c count  stop after N packets',
        cat:    'CAT(1)\n\nSYNOPSIS\n    cat [FILE]...\n\nDESCRIPTION\n    Concatenate files and print to stdout.',
        grep:   'GREP(1)\n\nSYNOPSIS\n    grep [-i] [-n] pattern file\n\nOPTIONS\n    -i  case insensitive\n    -n  show line numbers',
        rm:     'RM(1)\n\nSYNOPSIS\n    rm [-r] file...\n\nOPTIONS\n    -r  remove directories recursively',
      };
      return pages[args[0]] ? { out: pages[args[0]] } : { err: `No manual entry for ${args[0]}` };
    }

    case 'neofetch': {
      const days = Math.floor((new Date() - BIRTH) / 86400000);
      const now  = new Date();
      const hh   = String(now.getHours()).padStart(2, '0');
      const mm   = String(now.getMinutes()).padStart(2, '0');
      const G = 'color:var(--green);font-weight:bold';
      return { html: [
        `        <span style="${G}">/\\</span>          <span style="${G}">${USER}@${HOST}</span>`,
        `       <span style="${G}">/  \\</span>         <span class="c-dot">-----------</span>`,
        `      <span style="${G}">/\\   \\</span>        <span class="c-dot">OS:</span>       Arch Linux x86_64`,
        `     <span style="${G}">/      \\</span>       <span class="c-dot">Kernel:</span>   6.12.0-arch1-1`,
        `    <span style="${G}">/   ,,   \\</span>      <span class="c-dot">Uptime:</span>   ${days.toLocaleString('en')} days`,
        `   <span style="${G}">/   |  |   \\</span>     <span class="c-dot">Shell:</span>    bash 5.2.37`,
        `  <span style="${G}">/_-''    ''-_\\</span>    <span class="c-dot">CPU:</span>      Intel i7-9750H @ 2.6GHz`,
        `                   <span class="c-dot">Memory:</span>   4096MiB / 16384MiB`,
        `                   <span class="c-dot">Time:</span>     ${hh}:${mm}`,
      ].join('\n') };
    }

    case 'clear':
      outputArea.innerHTML = '';
      return null;

    case 'exit':
    case 'logout':
      return { out: `logout\n\nConnection to ${HOST} closed.` };

    case 'sudo':
      return { err: `${USER} is not in the sudoers file. This incident will be reported.` };

    case 'su':
      return { err: 'su: Authentication failure' };

    case 'vim': case 'vi': case 'nano': case 'emacs': case 'nvim':
      return { out: `${cmd}: graphical editor not available. Try: cat, grep, head, tail` };

    case 'sl':
      return { err: 'bash: sl: command not found\nDid you mean: ls' };

    case '':
      return null;

    case 'help':
      return { out: [
        'Available commands:',
        '  Navigation:  cd, ls [-la], pwd',
        '  Files:       cat, head [-n], tail [-n], grep [-in], wc [-lw], touch, mkdir, rm [-r]',
        '  System:      uname [-a], hostname, id, uptime, date, ps [aux]',
        '  Resources:   df [-h], free [-h]',
        '  Network:     ping [-c N] host, ip [addr|route], ifconfig',
        '  Info:        whoami, experience, contacts, neofetch, env',
        '  Other:       echo, history, which, man, clear, help',
        '',
        '  Tips:  Tab — completion,  ↑↓ — history,  Ctrl+C — interrupt',
      ].join('\n') };

    default:
      return { err: `bash: ${cmd}: command not found` };
  }
}

// ── Helpers ───────────────────────────────────────────────────────────────────
function headTailN(args, def) {
  const dash = args.find(a => /^-\d+$/.test(a));
  if (dash) return parseInt(dash.slice(1));
  const ni = args.indexOf('-n');
  if (ni !== -1 && args[ni + 1]) return parseInt(args[ni + 1]);
  return def;
}

// ── Ping (async, interruptible with Ctrl+C) ───────────────────────────────────
async function runPing(args) {
  let host  = null;
  let count = null;
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '-c' && args[i + 1]) { count = parseInt(args[++i]); }
    else if (!args[i].startsWith('-'))   { host  = args[i]; }
  }
  if (!host) { appendText('Usage: ping [-c count] destination', 'ln-err'); return; }

  const r  = () => Math.floor(Math.random() * 256);
  const ip = `${r() % 223 + 1}.${r()}.${r()}.${r()}`;
  appendText(`PING ${host} (${ip}) 56(84) bytes of data.`);

  const sig = { aborted: false };
  activeProc = { abort: () => { sig.aborted = true; } };

  let sent = 0;
  const rtts = [];
  while (count === null || sent < count) {
    await sleep(1000);
    if (sig.aborted) break;
    const rtt = +(15 + Math.random() * 35).toFixed(3);
    rtts.push(rtt);
    appendText(`64 bytes from ${ip}: icmp_seq=${sent} ttl=57 time=${rtt} ms`);
    sent++;
  }
  activeProc = null;

  appendText('');
  appendText(`--- ${host} ping statistics ---`);
  appendText(`${sent} packets transmitted, ${rtts.length} received, 0% packet loss`);
  if (rtts.length) {
    const min = Math.min(...rtts).toFixed(3);
    const max = Math.max(...rtts).toFixed(3);
    const avg = (rtts.reduce((a, b) => a + b) / rtts.length).toFixed(3);
    appendText(`rtt min/avg/max/mdev = ${min}/${avg}/${max}/0.000 ms`);
  }
}

// ── Input handler ─────────────────────────────────────────────────────────────
async function handleInput(raw) {
  appendRaw(promptHTML() + `<span class="p-cmd">${esc(raw)}</span>`, 'ln-prompt');

  if (raw.trim()) { hist.unshift(raw); histIdx = -1; }

  const parts = raw.trim().split(/\s+/);
  const cmd   = parts[0]?.toLowerCase() ?? '';
  const args  = parts.slice(1);

  if (cmd === 'ping') {
    await runPing(args);
  } else {
    appendResult(runCommand(cmd, args));
  }

  appendGap();
  scrollEnd();
  inputPrompt.innerHTML = promptHTML(); // обновить, если cwd изменился
}

// ── Interactive mode ──────────────────────────────────────────────────────────
function enableInput() {
  inputPrompt.innerHTML = promptHTML();
  inputRow.style.display = 'flex';
  liveInput.focus();

  document.addEventListener('click', () => liveInput.focus());
  liveInput.addEventListener('input', () => {
    inputDisplay.textContent = liveInput.value;
    scrollEnd();
  });

  liveInput.addEventListener('keydown', async e => {

    // Ctrl+C
    if (e.ctrlKey && e.key === 'c') {
      e.preventDefault();
      if (activeProc) {
        activeProc.abort();
        activeProc = null;
      } else {
        appendRaw(promptHTML() + '<span class="p-cmd"></span>', 'ln-prompt');
        liveInput.value = '';
        inputDisplay.textContent = '';
        appendGap();
      }
      histIdx = -1;
      return;
    }

    // История ↑
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (histIdx < hist.length - 1) histIdx++;
      liveInput.value = hist[histIdx] ?? '';
      inputDisplay.textContent = liveInput.value;
      return;
    }

    // История ↓
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (histIdx > 0) histIdx--;
      else if (histIdx === 0) { histIdx = -1; liveInput.value = ''; inputDisplay.textContent = ''; return; }
      liveInput.value = hist[histIdx] ?? '';
      inputDisplay.textContent = liveInput.value;
      return;
    }

    // Tab
    if (e.key === 'Tab') {
      e.preventDefault();
      const completed = tabComplete(liveInput.value);
      liveInput.value = completed;
      inputDisplay.textContent = completed;
      return;
    }

    // Enter
    if (e.key === 'Enter') {
      if (activeProc) return;
      const raw = liveInput.value;
      liveInput.value = '';
      inputDisplay.textContent = '';
      await handleInput(raw);
    }
  });
}

// ── Animated typing ───────────────────────────────────────────────────────────
async function typeCommand(cmd) {
  const row = appendRaw('', 'ln-prompt');
  row.innerHTML = promptHTML() +
    `<span class="p-cmd" id="__typing"></span>` +
    `<span class="cursor" id="__tcursor"></span>`;
  const span = document.getElementById('__typing');
  for (const ch of cmd) {
    await sleep(50 + Math.random() * 70);
    span.textContent += ch;
    scrollEnd();
  }
  document.getElementById('__tcursor')?.remove();
  span.removeAttribute('id');
}

// ── Boot ──────────────────────────────────────────────────────────────────────
async function loadFiles() {
  const files = ['whoami.md', 'experience.md', 'contacts.md'];
  await Promise.all(files.map(async name => {
    try {
      const text = await fetch(name).then(r => {
        if (!r.ok) throw new Error(r.status);
        return r.text();
      });
      VFS[`/home/evgenii/${name}`] = ['f', text.replace(/\r\n/g, '\n').trimEnd()];
    } catch (e) {
      VFS[`/home/evgenii/${name}`] = ['f', `(failed to load ${name})`];
    }
  }));
}

async function boot() {
  await loadFiles();
  await sleep(350);

  await typeCommand('whoami');
  await sleep(120);
  appendText(fread('/home/evgenii/whoami.md'));
  appendGap();
  await sleep(650);

  await typeCommand('experience');
  await sleep(120);
  appendText(fread('/home/evgenii/experience.md'));
  appendGap();
  await sleep(650);

  await typeCommand('contacts');
  await sleep(120);
  appendText(fread('/home/evgenii/contacts.md'));
  appendGap();
  await sleep(450);

  enableInput();
}

boot();
</script>
</body>
</html>
