/* header prompt → a tiny playful shell. type a command + Enter: known targets navigate, anything else is an easter egg (a fake shell error). the menu below is always "open" and clickable, so typing is optional. */ (function () { "use strict"; var bar = document.querySelector(".topbar"); var input = document.querySelector(".prompt__input"); var out = document.querySelector(".prompt__out"); var menu = document.querySelector(".menu"); var promptEl = document.querySelector(".prompt"); if (!bar || !input) return; // keep the caret/right end in view: when the line overflows, scroll it fully // right so the left (older) part clips off terminal-style instead of wrapping. function keepEnd() { if (promptEl) promptEl.scrollLeft = promptEl.scrollWidth; } // the lang-switch (de/en) is position:absolute top-right over the same row. // reserving space for it (or clipping the line early to dodge it) wastes // room on short lines — simpler to just hide it once a long line would // reach it, and let it reappear the moment the line is short again. wired // to a MutationObserver below so it re-checks on every change to the // prompt text, no matter what set it (ghost, real typing, history, …). function syncLangSwitch() { var ls = document.querySelector(".lang-switch"); if (!ls || !promptEl) return; var lsRect = ls.getBoundingClientRect(); var inRect = input.getBoundingClientRect(); ls.style.visibility = (lsRect.width && inRect.right > lsRect.left - 8) ? "hidden" : ""; } new MutationObserver(syncLangSwitch).observe(input, { characterData: true, childList: true, subtree: true }); // hard guarantee: the prompt is ALWAYS one line. CSS (nowrap + overflow:hidden) // is normally enough, but a long ghost command (long path + filename) can still // force the line to wrap on some viewports/browsers, which then bugs out the // layout below it. so on top of the CSS, actually shorten the text in the DOM: // clip from the FRONT (oldest part, same side as the terminal-scroll above) and // mark the cut with "…" — the leading verb ("cat "/"cd ") is allowed to go first. function fitOneLine(text) { if (!promptEl) return text; promptEl.scrollLeft = 0; // measure from a clean, unscrolled state input.textContent = text; if (promptEl.scrollWidth <= promptEl.clientWidth) return text; var shown = text; while (shown.length > 1 && promptEl.scrollWidth > promptEl.clientWidth) { shown = shown.slice(1); input.textContent = "…" + shown; } return input.textContent; } var home = bar.getAttribute("data-home") || "/"; // command → url: the always-open menu, plus any context routes (e.g. the // portfolio project slugs, only present in the DOM while inside /portfolio/). // rebuilt after each pjax navigation since .routes changes per section. var routes = {}; function buildRoutes() { routes = {}; Array.prototype.forEach.call(document.querySelectorAll(".menu a, .routes a, footer a[data-name], .lang-switch"), function (a) { var key = (a.getAttribute("data-name") || a.textContent).trim().toLowerCase().replace(/\/+$/, ""); if (key) routes[key] = a.getAttribute("href"); }); } buildRoutes(); document.addEventListener("pjax:load", buildRoutes); function pwd() { var p = document.querySelector(".prompt__path"); return p ? p.textContent.replace(/^:/, "").replace(/\$$/, "") : "~"; } function say(msg) { if (out) out.textContent = msg; } function navigate(u) { clearGhost(); input.textContent = ""; if (window.pjax) window.pjax(u); else window.location.href = u; } // ghost command: the file browser "types" e.g. `cat 03.webp` into the prompt as // you hover/freeze a file, so you always see where you are. it's only a hint — // any real keystroke (or navigation) discards it and the line is yours again. var ghostActive = false; function setGhost(text) { if (!ghostActive && input.textContent) return; // never clobber a real command in progress fitOneLine(text); ghostActive = true; input.classList.add("is-ghost"); caretToEnd(); } function clearGhost() { if (!ghostActive) return; input.textContent = ""; ghostActive = false; input.classList.remove("is-ghost"); } document.addEventListener("shell:ghost", function (e) { setGhost(e.detail); }); document.addEventListener("shell:ghost-clear", clearGhost); function pad(n) { return (n < 10 ? "0" : "") + n; } function dateStr() { var d = new Date(); var days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; var mons = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; return days[d.getDay()] + " " + mons[d.getMonth()] + " " + pad(d.getDate()) + " " + d.getFullYear(); } function timeStr() { var d = new Date(); return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()); } function infoStr() { var lines = []; var info = bar.getAttribute("data-info"); var email = bar.getAttribute("data-email"); if (info) lines.push(info); if (email) lines.push(email); lines.push("type: portfolio · about"); return lines.join("\n"); } // command history, kept for the whole tab session (survives navigation) var hist = []; try { hist = JSON.parse(sessionStorage.getItem("kgv_hist") || "[]"); } catch (e) { hist = []; } var hpos = hist.length; function remember(cmd) { if (hist[hist.length - 1] !== cmd) hist.push(cmd); hpos = hist.length; try { sessionStorage.setItem("kgv_hist", JSON.stringify(hist.slice(-100))); } catch (e) {} } function historyStr() { if (!hist.length) return "(empty)"; return hist.map(function (c, i) { return (" " + (i + 1)).slice(-4) + " " + c; }).join("\n"); } function unameStr(lc) { return lc === "uname -a" ? "kgv-sh 1.0 architecture lucerne ch" : "kgv-sh"; } // ls -a reveals the hidden dotfiles function lsa() { var items = ["./", "../"]; Array.prototype.forEach.call(document.querySelectorAll(".menu a"), function (a) { items.push((a.getAttribute("data-name") || "") + "/"); }); items.push(".colophon", ".legal-notice"); return items.join(" "); } function tree() { var work = []; Array.prototype.forEach.call(document.querySelectorAll(".routes a"), function (a) { work.push(a.getAttribute("data-name")); }); var l = ["."]; l.push("├── portfolio/"); work.forEach(function (s, i) { l.push("│ " + (i === work.length - 1 ? "└── " : "├── ") + s + "/"); }); l.push("├── about/"); l.push("├── .colophon"); l.push("└── .legal-notice"); return l.join("\n"); } function fetchStr() { var up = Math.max(1, Math.round((window.performance && performance.now ? performance.now() : 0) / 1000)); return [ " _ karimgabrielevarano", " | | __ -------------------", " | |/ / os: kgv-sh 1.0", " | < location: lucerne", " |_|\\_\\ degree: ba architecture", " status: ma in progress", " uptime: " + up + "s", " ~: portfolio · about" ].join("\n"); } function setTheme(mode) { var dark = mode === "dark"; document.documentElement.classList.toggle("dark", dark); try { localStorage.setItem("kgv_theme", dark ? "dark" : "light"); } catch (e) {} } // `sl` — a steam locomotive rolls across the screen (the classic `ls` typo gag) var TRAIN = [ " ==== ________ ___________ ", " _D _| |_______/ \\__I_I_____===__|_________| ", " |(_)--- | H\\________/ | | =|___ ___| ", " / | | H | | | | ||_| |_|| ", " | | | H |__--------------------| [___] | ", " | ________|___H__/__|_____/[][]~\\_______| | ", " |/ | |-----------I_____I [][] [] D |=======|__ ", "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", " |/-=|___|= || || || |_____/~\\___/ ", " \\_/ \\O=====O=====O=====O_/ \\_/ " ].join("\n"); function sl() { if (document.querySelector(".sl")) return; var pre = document.createElement("pre"); pre.className = "sl"; pre.textContent = TRAIN; document.body.appendChild(pre); pre.style.transform = "translateX(" + window.innerWidth + "px)"; requestAnimationFrame(function () { pre.style.transition = "transform 4s linear"; pre.style.transform = "translateX(-" + (pre.offsetWidth + 40) + "px)"; }); setTimeout(function () { pre.remove(); }, 4200); } // secret: retro games (doom, simcity, pacman, tetris) cd into a fake // "." folder and run ".sh" — archive.org DOSBox emulators // embedded inline in the same file-listing chrome as any other page (not a // floating window). ".." is a real link back to wherever you typed the // command from, so leaving is just normal navigation — nav.js's pjax swap // restores the real page (and real breadcrumb) on its own. archive.org // still gates each one behind its own "click to begin" — that's the // emulator requiring a real user gesture before it can play audio, a // cross-origin iframe we can't click into or configure around. var GAMES = { doom: "DoomsharewareEpisode", simcity: "msdos_SimCity_1989", pacman: "msdos_Pac-Man_1983", tetris: "msdos_Tetris_1987" }; function launchGame(name) { var archiveId = GAMES[name]; var main = document.getElementById("main-content"); if (!archiveId || !main || document.querySelector(".game-embed")) return; var back = window.location.pathname + window.location.search; var file = name + ".sh"; var pathEl = document.querySelector(".prompt__path"); if (pathEl && /\$$/.test(pathEl.innerHTML)) { pathEl.innerHTML = pathEl.innerHTML.replace(/\$$/, '/.' + name + '$'); } main.innerHTML = '
' + '

$ ls

' + '

total 1

' + '' + '
' + '
' + '' + '
' + '
' + file + ' — running
' + '
' + '
'; // re-run the same wiring a real pjax navigation would (file-row hover/ // click, route rebuilding) against this injected markup document.dispatchEvent(new CustomEvent("pjax:load")); // the home page's always-open menu lives in the header, not #main-content // — hide it too, otherwise its own "$ ls" sits right above the game's and // the listing looks doubled. restored on the next real navigation // (leaving the game), which also fires pjax:load — registered after the // dispatch above so it doesn't just catch its own event. if (menu && !menu.hidden) { menu.hidden = true; document.addEventListener("pjax:load", function unhide() { menu.hidden = false; document.removeEventListener("pjax:load", unhide); }, { once: true }); } } function egg(lc, raw) { var arg0 = raw.split(/\s+/)[0]; if (/^sudo\s+make\s+me\s+a\s+sandwich$/.test(lc)) return "okay."; if (/^make\s+me\s+a\s+sandwich$/.test(lc)) return "what? make it yourself."; if (/^sudo\b/.test(lc) || lc === "su") return "permission denied: nice try."; if (/^rm\b/.test(lc)) return "rm: some things you keep."; if (lc === "whoami") return "karim"; if (lc === "pwd") return pwd(); if (lc === "help" || /^man\b/.test(lc)) return "nav: portfolio · about · cv\nbasics: info · contact · date · time · tree · fetch · history\ntheme: dark · light"; if (lc === "hello" || lc === "hi" || lc === "hey") return "hi :)"; if (lc === "exit" || lc === "logout") return "there is no exit. only architecture."; if (lc === "coffee" || lc === "brew") return "☕ brewing… (418: i'm a teapot)"; if (/^(cat|open|vim|nano|emacs|less)\b/.test(lc)) return arg0 + ": permission denied"; return "zsh: command not found: " + arg0; } // tab-completion over known commands + navigable routes var COMMANDS = ["help", "man", "info", "contact", "date", "time", "tree", "fetch", "neofetch", "uname", "history", "clear", "echo", "pwd", "whoami", "ls", "cd", "cat", "open", "dark", "light", "theme", "sl", "doom", "simcity", "pacman", "tetris", "coffee"]; function complete() { var val = input.textContent; var parts = val.split(" "); var last = (parts[parts.length - 1] || "").toLowerCase(); if (!last) return; var pool = parts.length > 1 ? Object.keys(routes) : COMMANDS.concat(Object.keys(routes)); var matches = []; for (var i = 0; i < pool.length; i++) { if (pool[i].indexOf(last) === 0 && matches.indexOf(pool[i]) < 0) matches.push(pool[i]); } if (!matches.length) return; var common = matches.reduce(function (a, b) { var n = 0; while (n < a.length && a.charAt(n) === b.charAt(n)) n++; return a.slice(0, n); }); parts[parts.length - 1] = matches.length === 1 ? matches[0] : common; input.textContent = parts.join(" "); caretToEnd(); say(matches.length > 1 ? matches.join(" ") : ""); // list options when ambiguous } function run(raw) { var cmd = raw.replace(/\s+/g, " ").trim(); if (!cmd) return; var lc = cmd.toLowerCase(); remember(cmd); if (lc === "clear") { say(""); input.textContent = ""; return; } if (lc === "doom" || lc === "simcity" || lc === "tetris") { launchGame(lc); say(""); input.textContent = ""; return; } if (lc === "pacman" || lc === "pac-man") { launchGame("pacman"); say(""); input.textContent = ""; return; } if (lc === "sl") { sl(); say(""); input.textContent = ""; return; } if (lc === "dark" || lc === "light") { setTheme(lc); say(""); input.textContent = ""; return; } if (lc === "theme") { var next = document.documentElement.classList.contains("dark") ? "light" : "dark"; setTheme(next); say("theme: " + next); input.textContent = ""; return; } // strip a leading verb, leading ./ and trailing slash var t = lc.replace(/^(cd|ls|cat|open)\b\s*/, "").replace(/^\.\//, "").replace(/\/+$/, "").trim(); // `ls` of the current/home dir — the listing is already on screen if (/^ls\b/.test(lc) && (t === "" || t === "~" || t === ".")) { say(""); input.textContent = ""; return; } // home / up if (t === "" || t === "~" || t === ".." || t === "." || t === "home" || t === "/") { navigate(home); return; } // a known menu target if (routes[t]) { navigate(routes[t]); return; } // basics that just print if (lc === "info") { say(infoStr()); input.textContent = ""; return; } if (lc === "contact") { say(bar.getAttribute("data-email") || ""); input.textContent = ""; return; } if (lc === "date") { say(dateStr()); input.textContent = ""; return; } if (lc === "time") { say(timeStr()); input.textContent = ""; return; } if (lc === "history") { say(historyStr()); input.textContent = ""; return; } if (lc === "tree") { say(tree()); input.textContent = ""; return; } if (lc === "fetch" || lc === "neofetch") { say(fetchStr()); input.textContent = ""; return; } if (lc === "uname" || lc === "uname -a") { say(unameStr(lc)); input.textContent = ""; return; } if (/^ls\s+-(la|al|a)$/.test(lc) || lc === "ls --all") { say(lsa()); input.textContent = ""; return; } if (lc === "echo" || lc.indexOf("echo ") === 0) { say(cmd.slice(4).trim()); input.textContent = ""; return; } say(egg(lc, cmd)); // unknown → shell error, fresh empty prompt input.textContent = ""; } input.addEventListener("keydown", function (e) { // a real keystroke wins over the file-browser's ghost command if (ghostActive && !/^(Shift|Control|Alt|Meta|CapsLock)$/.test(e.key)) clearGhost(); if (e.key === "Enter") { e.preventDefault(); run(input.textContent); return; } if (e.key === "Tab") { e.preventDefault(); complete(); return; } if (e.key === "ArrowUp" && hist.length) { e.preventDefault(); hpos = Math.max(0, hpos - 1); input.textContent = hist[hpos] || ""; caretToEnd(); say(""); } else if (e.key === "ArrowDown" && hist.length) { e.preventDefault(); hpos = Math.min(hist.length, hpos + 1); input.textContent = hist[hpos] || ""; caretToEnd(); say(""); } }); input.addEventListener("input", function () { say(""); keepEnd(); }); function caretToEnd() { var r = document.createRange(); r.selectNodeContents(input); r.collapse(false); var s = window.getSelection(); s.removeAllRanges(); s.addRange(r); keepEnd(); } // start typing anywhere → the keystroke goes straight into the command line document.addEventListener("keydown", function (e) { if (document.querySelector(".doom-window")) return; // playing doom var t = e.target; if (t === input) return; // already typing if (t && (t.isContentEditable || /^(input|textarea|select)$/i.test(t.tagName))) return; if (e.metaKey || e.ctrlKey || e.altKey) return; if (e.key && e.key.length === 1) { // a printable character e.preventDefault(); if (ghostActive) clearGhost(); // discard the ghost, type for real input.focus(); input.textContent += e.key; caretToEnd(); say(""); } }); // clicking anywhere on the bar focuses the prompt (but let links + text selection through) bar.addEventListener("click", function (e) { if (e.target.closest("a")) return; if (window.getSelection && String(window.getSelection())) return; input.focus(); caretToEnd(); }); })();