1import { WORK, PROJECTS, PUBLICATIONS, EXTRAS, parsePeriod, formatDuration } from './data.js'; 2 3/* ââ DOM helpers â no innerHTML ââ */ 4 5function el(tag, cls) { 6 const e = document.createElement(tag); 7 if (cls) e.className = cls; 8 return e; 9} 10function text(s) { return document.createTextNode(s); } 11function span(cls, s) { const e = el('span', cls); if (s !== undefined) e.textContent = s; return e; } 12function div(cls) { return el('div', cls); } 13function a(href, s, cls) { 14 const e = el('a', cls); 15 e.href = href; 16 e.target = '_blank'; 17 e.rel = 'noopener noreferrer'; 18 e.textContent = s; 19 return e; 20} 21 22/* ââ Terminal state ââ */ 23 24const output = document.getElementById('output'); 25const promptText = document.getElementById('prompt-text'); 26const realInput = document.getElementById('realInput'); 27 28let cmdHistory = []; 29let histIdx = -1; 30let busy = false; 31 32/* ââ Boot sequence ââ */ 33 34document.querySelectorAll('.boot-line').forEach(line => { 35 const delay = parseInt(line.dataset.delay || '0', 10); 36 setTimeout(() => line.classList.add('show'), delay); 37}); 38 39/* ââ Output helpers ââ */ 40 41function echoCmd(cmd) { 42 const block = div('cmd-block'); 43 const echo = div('cmd-echo'); 44 echo.appendChild(span('sigil', 'iordan@tokyo:~$')); 45 echo.appendChild(span('typed', cmd)); 46 block.appendChild(echo); 47 const result = div('result'); 48 block.appendChild(result); 49 output.appendChild(block); 50 return result; 51} 52 53function revealLines(container, children, baseDelay = 80, step = 60) { 54 children.forEach((child, i) => { 55 child.style.opacity = '0'; 56 child.style.transform = 'translateY(6px)'; 57 child.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; 58 container.appendChild(child); 59 setTimeout(() => { 60 child.style.opacity = '1'; 61 child.style.transform = 'translateY(0)'; 62 }, baseDelay + i * step); 63 }); 64 setTimeout(() => { output.scrollTop = output.scrollHeight; }, baseDelay + children.length * step); 65} 66 67function scrollBottom() { 68 setTimeout(() => { output.scrollTop = output.scrollHeight; }, 50); 69} 70 71/* ââââââââââââââââââââââââââââââââââââââââââ 72 COMMAND HANDLERS 73ââââââââââââââââââââââââââââââââââââââââââ */ 74 75function cmdWhoami(result) { 76 const wrap = div('r-whoami'); 77 const layout = div('r-whoami-layout'); 78 79 const portraitCol = div('r-portrait-col'); 80 const portrait = document.createElement('img'); 81 portrait.src = 'img/me.jpg'; 82 portrait.alt = 'Iordan Iordanov'; 83 portrait.className = 'r-portrait'; 84 portraitCol.appendChild(portrait); 85 portraitCol.appendChild(div('r-portrait-glow')); 86 layout.appendChild(portraitCol); 87 88 const textCol = div('r-whoami-text-col'); 89 90 const name = div('r-name'); 91 name.appendChild(text('Iordan ')); 92 const em = document.createElement('em'); 93 em.textContent = 'Iordanov'; 94 name.appendChild(em); 95 textCol.appendChild(name); 96 97 const tagline = div('r-tagline'); 98 tagline.textContent = 'Making sense of things, one breakdown at a time.'; 99 textCol.appendChild(tagline); 100 101 const bio = div('r-bio'); 102 bio.appendChild(text('I do whatever needs to be done: research, code, architecture, management, counseling, to name a few. Breaking and rebuilding is the best way for me to learn. I care about impact, not titles. These days I\'m at ')); 103 bio.appendChild(a('https://braid.tech', 'Braid Technologies', null)); 104 bio.appendChild(text(', working on geometry generation and processing for manufacturing. Before that, I was the CTO at ')); 105 bio.appendChild(a('https://corpy.co.jp', 'Corpy&Co.', null)); 106 bio.appendChild(text(', where I shipped 50+ projects to clients while building an MLOps platform. Before that, I did a Ph.D. in Computational Geometry, which is now part of ')); 107 bio.appendChild(a('https://www.cgal.org/2019/02/25/Hyperbolic_triangulations/', 'CGAL', null)); 108 bio.appendChild(text('.')); 109 textCol.appendChild(bio); 110 111 const facts = div('r-facts'); 112 const factData = [ 113 ['based', 'Tokyo, Japan'], 114 ['origin', 'Bulgaria'], 115 ['languages', [
116 'Native (but rusty) in Greek and Bulgarian', 117 'Fluent in English and Italian', 118 'Confident in French', 119 'Hopeless in Japanese', 120 'Bonus minority language available upon request', 121 ]], 122 ['lived in', 'Bulgaria · Greece · Italy · France · Japan'], 123 ]; 124 factData.forEach(([key, val]) => { 125 const row = div('r-fact-row'); 126 row.appendChild(span('r-fact-key', key)); 127 if (Array.isArray(val)) { 128 const list = el('ul', 'r-fact-list'); 129 val.forEach(item => { const li = el('li'); li.textContent = item; list.appendChild(li); }); 130 row.appendChild(list); 131 } else { 132 row.appendChild(span('r-fact-val', val)); 133 } 134 facts.appendChild(row); 135 }); 136 textCol.appendChild(facts); 137 138 const edu = div('r-edu'); 139 const eduEntries = [ 140 { degree: 'Ph.D. Computer Science', school: 'Université de Lorraine', year: '2016 ~ 2018', thesis: 'Delaunay triangulations of hyperbolic surfaces', thesisHref: 'docs/PhD-thesis.pdf' }, 141 { degree: 'M.Sc. Applied Mathematics', school: 'University of Crete', year: '2014 ~ 2015', thesis: 'Shape-Preserving Interpolation on the Sphere', thesisHref: 'docs/MSc-thesis.pdf' }, 142 { degree: 'B.Sc. Applied Mathematics', school: 'University of Crete', year: '2005 ~ 2013', thesis: 'The Euclidean InSphere Predicate', thesisHref: 'docs/BSc-thesis.pdf', 143 note: 'Studies interrupted 2007 ~ 2009 for work in Italy' }, 144 ]; 145 eduEntries.forEach(e => { 146 const item = div('r-edu-item'); 147 const top = div('r-edu-top'); 148 top.appendChild(span('r-edu-degree', e.degree)); 149 top.appendChild(span('r-edu-year', e.year)); 150 item.appendChild(top); 151 const school = div('r-edu-school'); 152 school.textContent = e.school; 153 item.appendChild(school); 154 if (e.note) { const note = div('r-edu-note'); note.textContent = e.note; item.appendChild(note); } 155 item.appendChild(a(e.thesisHref, 'â ' + e.thesis, 'r-edu-thesis')); 156 edu.appendChild(item); 157 }); 158 textCol.appendChild(edu); 159 160 layout.appendChild(textCol); 161 wrap.appendChild(layout); 162 163 const hint = div('back-sug'); 164 hint.appendChild(text('â ')); 165 [['ls work/', 'ls work/'], ['ls projects/', 'ls projects/']].forEach(([label, cmd]) => { 166 const btn = document.createElement('button'); 167 btn.textContent = label; 168 btn.addEventListener('click', () => typeCommand(cmd)); 169 hint.appendChild(btn); 170 hint.appendChild(text(' ')); 171 }); 172 wrap.appendChild(hint); 173 174 result.appendChild(wrap); 175 scrollBottom(); 176} 177 178function cmdLsWork(result) { 179 const wrap = div('r-ls'); 180 const header = div('ls-header'); 181 header.textContent = 'PERIOD ORG NAME DURATION'; 182 wrap.appendChild(header); 183 184 const rows = WORK.map(w => { 185 const months = parsePeriod(w.period); 186 const row = div('ls-entry'); 187 row.setAttribute('role', 'button'); 188 row.setAttribute('tabindex', '0'); 189 row.setAttribute('aria-label', `${w.org}, ${w.name}, ${w.period}`); 190 row.appendChild(span('ls-period', w.period)); 191 row.appendChild(span('ls-org', w.org)); 192 row.appendChild(span('ls-name', w.name)); 193 const badges = div('ls-fields'); 194 (w.fields || []).forEach(f => badges.appendChild(span('ls-field-badge', f))); 195 row.appendChild(badges); 196 row.appendChild(span('ls-duration', months ? formatDuration(months) : '')); 197 const activate = () => typeCommand('cat ' + w.id); 198 row.addEventListener('click', activate); 199 row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(); } }); 200 return row; 201 }); 202 203 const fieldTotals = {}; 204 WORK.forEach(w => { 205 const fs = w.fields?.length ? w.fields : ['Other']; 206 const m = parsePeriod(w.period) || 0; 207 fs.forEach(f => { fieldTotals[f] = (fieldTotals[f] || 0) + m; }); 208 }); 209 const fieldOrder = ['Software Dev', 'Research', 'AI / ML', 'Management', 'Other']; 210 const totalsRow = div('ls-field-totals'); 211 fieldOrder.forEach(f => { 212 if (!fieldTotals[f]) return; 213 const item = span('ls-field-total-item', ''); 214 item.appendChild(span('ls-field-label', f)); 215 item.appendChild(span('ls-field-val', formatDuration(fieldTotals[f]))); 216 totalsRow.appendChild(item); 217 }); 218 219 revealLines(wrap, rows, 60, 50); 220 setTimeout(() => { 221 wrap.appendChild(totalsRow); 222 totalsRow.style.opacity = '0'; 223 totalsRow.style.transition = 'opacity 0.4s ease'; 224 setTimeout(() => { totalsRow.style.opacity = '1'; }, 60 + rows.length * 50 + 100); 225 }, 0); 226 result.appendChild(wrap); 227 scrollBottom(); 228} 229 230function cmdLsProjects(result) { 231 const wrap = div('r-ls'); 232 const header = div('ls-header'); 233 header.textContent = 'PERIOD ORG NAME'; 234 wrap.appendChild(header); 235 236 const rows = PROJECTS.map(p => {
237 const row = div('ls-entry'); 238 row.setAttribute('role', 'button'); 239 row.setAttribute('tabindex', '0'); 240 row.setAttribute('aria-label', `${p.org}, ${p.name}, ${p.period}`); 241 row.appendChild(span('ls-period', p.period)); 242 row.appendChild(span('ls-org', p.org)); 243 row.appendChild(span('ls-name', p.name)); 244 const activate = () => typeCommand('cat ' + p.id); 245 row.addEventListener('click', activate); 246 row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(); } }); 247 return row; 248 }); 249 250 revealLines(wrap, rows, 60, 50); 251 result.appendChild(wrap); 252 scrollBottom(); 253} 254 255function cmdCat(id, result) { 256 const entry = [...WORK, ...PROJECTS].find(e => e.id === id); 257 if (!entry) { 258 const err = div('r-error'); 259 err.textContent = 'cat: ' + id + ': no such file'; 260 result.appendChild(err); 261 scrollBottom(); 262 return; 263 } 264 265 const wrap = div('r-cat'); 266 const header = div('r-cat-header'); 267 const role = div('r-cat-role'); 268 role.textContent = entry.role; 269 header.appendChild(role); 270 271 const org = div('r-cat-org'); 272 if (entry.orgUrl) org.appendChild(a(entry.orgUrl, entry.org, null)); 273 else org.textContent = entry.org; 274 header.appendChild(org); 275 276 const isWork = WORK.some(w => w.id === id); 277 const months = isWork ? parsePeriod(entry.period) : null; 278 const meta = div('r-cat-meta'); 279 meta.textContent = entry.meta + ' · ' + entry.period + (months ? ' · ' + formatDuration(months) : ''); 280 header.appendChild(meta); 281 wrap.appendChild(header); 282 283 const body = div('r-cat-body'); 284 body.textContent = entry.body; 285 wrap.appendChild(body); 286 287 const tags = div('r-cat-tags');
288 entry.tags.forEach(t => tags.appendChild(span('r-cat-tag', t))); 289 wrap.appendChild(tags); 290 291 let links = null; 292 if (entry.links?.length) { 293 links = div('r-cat-links'); 294 entry.links.forEach(l => links.appendChild(a(l.href, l.label, 'out-link'))); 295 wrap.appendChild(links); 296 } 297 298 const backCmd = PROJECTS.some(p => p.id === id) ? 'ls projects/' : 'ls work/'; 299 const hint = div('back-sug'); 300 hint.appendChild(text('â ')); 301 const btn = document.createElement('button'); 302 btn.textContent = backCmd; 303 btn.addEventListener('click', () => typeCommand(backCmd)); 304 hint.appendChild(btn); 305 wrap.appendChild(hint); 306 307 const children = [header, body, tags, ...(links ? [links] : []), hint]; 308 children.forEach(c => { 309 c.style.opacity = '0'; 310 c.style.transform = 'translateY(6px)'; 311 c.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; 312 }); 313 result.appendChild(wrap); 314 children.forEach((c, i) => setTimeout(() => { c.style.opacity = '1'; c.style.transform = 'translateY(0)'; }, 80 + i * 70)); 315 scrollBottom(); 316} 317 318function cmdWget(result) { 319 const wrap = div('r-writing'); 320 const status = div('r-wget-status'); 321 [ 322 'Connecting to imiordanov.substack.com...', 323 'HTTP request sent, awaiting response... 200 OK', 324 'Length: unspecified [text/html]', 325 'Saving to: oni-no-pantsu.html', 326 ].forEach(l => { const line = div('r-wget-line'); line.textContent = l; status.appendChild(line); }); 327 wrap.appendChild(status); 328 329 const titleLink = a('https://imiordanov.substack.com', '', null); 330 titleLink.style.textDecoration = 'none'; 331 const jp = div('r-jp'); jp.textContent = '鬼ã®ãã³ã'; titleLink.appendChild(jp); 332 const romaji = div('r-romaji'); romaji.textContent = 'oni no pantsu · the demon\'s pants'; titleLink.appendChild(romaji); 333 wrap.appendChild(titleLink); 334 335 const quote = div('r-writing-quote'); 336 quote.textContent = 'The personal journal of a foreigner in Japan. Fatherhood, career, cultural shocks, mental health (and lack thereof) -- I\'ve got it all covered. Come along and laugh at my struggles.'; 337 wrap.appendChild(quote); 338 339 const body = div('r-writing-body'); 340 body.textContent = 'I write on Substack about the things that don\'t fit anywhere else.'; 341 wrap.appendChild(body); 342 343 wrap.appendChild(a('https://imiordanov.substack.com', 'â imiordanov.substack.com', 'out-link amber')); 344 result.appendChild(wrap); 345 scrollBottom(); 346} 347 348function cmdGrep(query, result) { 349 const wrap = div('r-grep'); 350 const matches = query 351 ? PUBLICATIONS.filter(p => (p.title + ' ' + p.authors + ' ' + p.venue).toLowerCase().includes(query.toLowerCase())) 352 : PUBLICATIONS; 353 354 if (!matches.length) { 355 const err = div('r-error'); 356 err.textContent = 'grep: no matches found for "' + query + '"'; 357 result.appendChild(err); 358 scrollBottom(); 359 return; 360 } 361 362 const header = div('r-grep-header'); 363 header.textContent = query 364 ? `publications.bib:${matches.length} match${matches.length > 1 ? 'es' : ''} for "${query}"` 365 : `publications.bib:${matches.length} entries`; 366 wrap.appendChild(header); 367 368 const rows = matches.map(p => { 369 const row = div('r-grep-row'); 370 const meta = div('r-grep-meta'); 371 meta.appendChild(span('r-grep-ref', p.ref)); 372 row.appendChild(meta); 373 const titleEl = a(p.href, p.title, 'r-grep-title'); 374 titleEl.target = '_blank'; titleEl.rel = 'noopener noreferrer'; 375 row.appendChild(titleEl); 376 const authors = div('r-grep-authors'); authors.textContent = p.authors; row.appendChild(authors); 377 const venue = div('r-grep-venue'); venue.textContent = p.venue; row.appendChild(venue); 378 if (p.note) { const note = div('r-grep-note'); note.textContent = p.note; row.appendChild(note); } 379 return row; 380 }); 381 382 revealLines(wrap, rows, 60, 70); 383 result.appendChild(wrap); 384 scrollBottom(); 385} 386 387function cmdMan(result) { 388 const wrap = div('r-man'); 389 const sections = [ 390 { key: 'NAME', val: 'iordan â researcher, builder, chronic overthinker' }, 391 { key: 'SYNOPSIS', val: 'iordan [--geometry] [--ai] [--software] [--research]' }, 392 { key: 'DESCRIPTION', val: 'Researcher at Braid Technologies, currently working on geometry generation and design automation for manufacturing. Former CTO at Corpy&Co., scaling a team from 10 to 70 engineers and delivering over 50 client-facing projects. Ph.D. in Computational Geometry from Université de Lorraine, France. Contributor to CGAL, the largest open-source library for computational geometry.' }, 393 { key: 'HISTORY', val: 'Born in Bulgaria, of Linguri/Rudari heritage. Spent 13 years in Greece, then 3 years in Italy, then 3 in France, and moved to Japan in 2019. Became a husband in the times of COVID. Became a father of a very energetic boy in 2024.' }, 394 { key: 'LANGUAGES', val: 'Bulgarian (native), Greek (native), English (fluent), Italian (fluent), French (advanced), Linguri (native), Japanese (currently faking it, not sure I\'ll ever make it)' }, 395 { key: 'BUGS', val: 'Anxiety. Opinionated idealism. Dad jokes. No music taste.' }, 396 { key: 'SEE ALSO', val: 'whoami(1), ping(8), grep(1), ls(1), wget(1)' }, 397 ]; 398 const els = sections.map(s => { 399 const item = div('r-man-section'); 400 item.appendChild(span('r-man-key', s.key)); 401 const val = div('r-man-val'); val.textContent = s.val; item.appendChild(val); 402 return item; 403 }); 404 revealLines(wrap, els, 60, 60); 405 result.appendChild(wrap); 406 scrollBottom(); 407} 408 409function cmdPing(result) { 410 const wrap = div('r-ping'); 411 const header = div('r-ping-header'); 412 header.textContent = 'PING iordan (imiordanov.github.io): current location & contact'; 413 wrap.appendChild(header); 414 415 const rows = [ 416 { key: 'location', val: 'Tokyo, Japan' }, 417 { key: 'role', val: 'Researcher @ Braid Technologies' }, 418 { key: 'status', val: 'open to interesting conversations' }, 419 { key: 'email', val: '[email protected]', href: 'mailto:[email protected]' }, 420 { key: 'email alt', val: '[email protected]', href: 'mailto:[email protected]' }, 421 { key: 'linkedin', val: 'linkedin.com/in/imiordanov', href: 'https://www.linkedin.com/in/imiordanov/' }, 422 { key: 'github', val: 'github.com/imiordanov', href: 'https://github.com/imiordanov' }, 423 { key: 'substack', val: 'imiordanov.substack.com', href: 'https://imiordanov.substack.com' }, 424 ]; 425 const rowEls = rows.map(r => {
426 const row = div('r-ping-row'); 427 row.appendChild(span('r-ping-key', r.key)); 428 if (r.href) { 429 const lnk = a(r.href, r.val, null); 430 if (!r.href.startsWith('mailto:')) { lnk.target = '_blank'; lnk.rel = 'noopener noreferrer'; } 431 row.appendChild(lnk); 432 } else { 433 row.appendChild(span('r-ping-val', r.val)); 434 } 435 return row; 436 }); 437 revealLines(wrap, rowEls, 60, 80); 438 result.appendChild(wrap); 439 scrollBottom(); 440} 441 442function cmdLsExtras(result) { 443 const wrap = div('r-ls'); 444 const header = div('ls-header'); 445 header.textContent = 'NAME DESCRIPTION'; 446 wrap.appendChild(header); 447 448 const rows = EXTRAS.map(e => { 449 const row = div('ls-entry'); 450 row.style.gridTemplateColumns = '160px 1fr'; 451 row.setAttribute('role', 'button'); 452 row.setAttribute('tabindex', '0'); 453 row.setAttribute('aria-label', `${e.name}, ${e.desc}`); 454 row.appendChild(span('ls-name', e.name)); 455 row.appendChild(span('ls-org', e.desc)); 456 const activate = () => typeCommand('open ' + e.id); 457 row.addEventListener('click', activate); 458 row.addEventListener('keydown', ev => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); activate(); } }); 459 return row; 460 }); 461 revealLines(wrap, rows, 60, 50); 462 result.appendChild(wrap); 463 scrollBottom(); 464} 465 466function cmdOpen(id, result) { 467 const extra = EXTRAS.find(e => e.id === id); 468 if (!extra) { 469 const err = div('r-error'); 470 err.textContent = 'open: ' + id + ': no such file â try ls extras/'; 471 result.appendChild(err); 472 scrollBottom(); 473 return; 474 } 475 const wrap = div('r-contact'); 476 const row = div('r-contact-line'); 477 row.appendChild(span('ck', 'opening')); 478 const lnk = a(extra.href, extra.name, null); 479 lnk.target = '_blank'; lnk.rel = 'noopener noreferrer'; 480 row.appendChild(lnk); 481 wrap.appendChild(row); 482 result.appendChild(wrap); 483 setTimeout(() => window.open(extra.href, '_blank'), 600); 484 scrollBottom(); 485} 486 487function cmdCv(result) { 488 const wrap = div('r-contact'); 489 const row = div('r-contact-line'); 490 row.appendChild(span('ck', 'cv')); 491 const lnk = a('docs/cv-iordanov.pdf', 'cv-iordanov.pdf', null); 492 lnk.target = '_blank'; 493 row.appendChild(lnk); 494 wrap.appendChild(row); 495 const note = div('back-sug'); 496 note.style.marginTop = '14px'; 497 note.textContent = 'or try: ls work/ for an interactive version'; 498 wrap.appendChild(note); 499 result.appendChild(wrap); 500 scrollBottom(); 501} 502 503function cmdHelp(result) { 504 const wrap = div('r-help'); 505 const cmds = [ 506 ['whoami', 'brief identity'], 507 ['man iordan', 'full manual page'], 508 ['ls work/', 'work history'], 509 ['ls projects/', 'research & side projects'], 510 ['ls extras/', 'other things worth your time'], 511 ['cat <id>', 'read a work or project entry (click any row, or type the id)'], 512 ['open <id>', 'open an extra â try: open game-of-life'], 513 ['wget writing', 'fetch the Substack journal'], 514 ['grep <query>', 'search publications (or grep alone for all)'], 515 ['ping iordan', 'current location, role & contact'], 516 ['cv', 'download curriculum vitae'], 517 ['clear', 'clear the terminal'], 518 ['help', 'show this message'], 519 ]; 520 const rows = cmds.map(([cmd, desc]) => { 521 const row = div('r-help-row'); 522 row.appendChild(span('r-help-cmd', cmd)); 523 row.appendChild(span('r-help-desc', desc)); 524 return row; 525 }); 526 revealLines(wrap, rows, 40, 40); 527 result.appendChild(wrap); 528 scrollBottom(); 529} 530 531function cmdClear() { 532 while (output.firstChild) output.removeChild(output.firstChild); 533} 534 535function cmdUnknown(cmd, result) { 536 const err = div('r-error'); 537 err.textContent = 'command not found: ' + cmd + ' â type help for available commands'; 538 result.appendChild(err); 539 scrollBottom(); 540} 541 542/* ââ Router ââ */ 543 544function route(raw) { 545 const cmd = raw.trim(); 546 const lower = cmd.toLowerCase(); 547 const result = echoCmd(cmd); 548 549 if (lower === 'whoami') return cmdWhoami(result); 550 if (lower === 'man iordan' || lower === 'man') return cmdMan(result); 551 if (lower === 'ping iordan' || lower === 'ping') return cmdPing(result); 552 if (lower === 'ls work/' || lower === 'ls work') return cmdLsWork(result); 553 if (lower === 'ls projects/' || lower === 'ls projects') return cmdLsProjects(result); 554 if (lower === 'ls extras/' || lower === 'ls extras') return cmdLsExtras(result); 555 if (lower === 'ls') return cmdLsWork(result); 556 if (lower === 'wget writing' || lower === 'wget') return cmdWget(result); 557 if (lower === 'cv') return cmdCv(result); 558 if (lower === 'help') return cmdHelp(result); 559 if (lower === 'clear') return cmdClear(); 560 if (lower.startsWith('grep')) { 561 const query = cmd.slice(4).trim(); 562 if (!query) { 563 const typedEl = result.previousElementSibling?.querySelector('.typed'); 564 if (typedEl) typedEl.textContent = 'grep . publications.bib'; 565 } 566 return cmdGrep(query, result); 567 } 568 if (lower.startsWith('open ')) return cmdOpen(lower.slice(5).trim(), result); 569 if (lower.startsWith('cat ')) return cmdCat(lower.slice(4).trim(), result); 570 cmdUnknown(cmd, result); 571} 572 573/* ââ Typewriter effect ââ */ 574 575function typeCommand(cmd) { 576 if (busy) return; 577 busy = true; 578 realInput.blur(); 579 promptText.textContent = ''; 580 let i = 0; 581 function typeChar() { 582 if (i < cmd.length) { 583 promptText.textContent += cmd[i++]; 584 setTimeout(typeChar, 38 + Math.random() * 30); 585 } else { 586 setTimeout(() => { 587 promptText.textContent = ''; 588 cmdHistory.unshift(cmd); 589 histIdx = -1; 590 route(cmd); 591 busy = false;
592 realInput.focus(); 593 }, 180); 594 } 595 } 596 typeChar(); 597} 598 599/* ââ Keyboard input ââ */ 600 601realInput.addEventListener('keydown', e => { 602 if (busy) { e.preventDefault(); return; } 603 604 if (e.key === 'Enter') { 605 const val = realInput.value.trim(); 606 realInput.value = ''; 607 promptText.textContent = ''; 608 if (!val) return; 609 cmdHistory.unshift(val); 610 histIdx = -1; 611 route(val); 612 return; 613 } 614 615 if (e.key === 'ArrowUp') { 616 e.preventDefault(); 617 if (histIdx < cmdHistory.length - 1) { 618 histIdx++; 619 realInput.value = promptText.textContent = cmdHistory[histIdx]; 620 } 621 return; 622 } 623 624 if (e.key === 'ArrowDown') { 625 e.preventDefault(); 626 if (histIdx > 0) { 627 histIdx--; 628 realInput.value = promptText.textContent = cmdHistory[histIdx]; 629 } else { 630 histIdx = -1; 631 realInput.value = promptText.textContent = ''; 632 } 633 return; 634 } 635 636 if (e.key === 'Tab') { 637 e.preventDefault(); 638 const val = realInput.value.toLowerCase(); 639 const allCmds = [ 640 'whoami', 'man iordan', 'ping iordan', 641 'ls work/', 'ls projects/', 'ls extras/', 642 'wget writing', 'grep', 'cv', 'help', 'clear', 643 ...[...WORK, ...PROJECTS].map(e => 'cat ' + e.id), 644 ...EXTRAS.map(e => 'open ' + e.id), 645 ]; 646 const match = allCmds.find(c => c.startsWith(val) && c !== val); 647 if (match) realInput.value = promptText.textContent = match; 648 return; 649 } 650}); 651 652realInput.addEventListener('input', () => { promptText.textContent = realInput.value; }); 653 654/* ââ Suggestion pills ââ */ 655 656document.querySelectorAll('.sug').forEach(btn => { 657 btn.addEventListener('click', () => { if (btn.dataset.cmd) typeCommand(btn.dataset.cmd); }); 658}); 659 660/* ââ Click anywhere to focus input ââ */ 661 662document.addEventListener('click', e => { 663 if (!e.target.closest('a') && !e.target.closest('button') && !e.target.closest('.ls-entry')) { 664 realInput.focus(); 665 } 666}); 667 668/* ââ Boot: auto-run whoami ââ */ 669 670setTimeout(() => { realInput.focus(); typeCommand('whoami'); }, 700);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.