/* global React, ReactDOM */ const { useState, useEffect, useRef, useCallback, useMemo } = React; // ============================================================ // Themes — each named after a real vintage display // ============================================================ const THEMES = [ { id: "ibm-5151", name: "IBM 5151", year: 1983, desc: "MDA amber phosphor monitor", swatch: "#ffb000", }, { id: "dec-vt220", name: "DEC VT220", year: 1983, desc: "P1 green phosphor terminal", swatch: "#33ff66", }, { id: "commodore-64", name: "Commodore 64", year: 1982, desc: "light blue on navy boot screen", swatch: "#8a9bff", }, { id: "hercules", name: "Hercules HGC", year: 1982, desc: "monochrome white phosphor card", swatch: "#d4e6ff", }, { id: "apple-ii", name: "Apple ][", year: 1977, desc: "warm green composite monitor", swatch: "#4cff8c", }, { id: "plasma", name: "IBM PS/55", year: 1989, desc: "gas-plasma orange display", swatch: "#ff6a2a", }, ]; function setTheme(id) { document.body.setAttribute("data-theme", id); } function getTheme() { return document.body.getAttribute("data-theme") || "ibm-5151"; } // ============================================================ // Works data — loaded from works.json // ============================================================ let WORKS = []; let WORKS_LOADED = false; let WORKS_ERR = null; async function loadWorks() { try { const res = await fetch("works.json", { cache: "no-cache" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); WORKS = await res.json(); WORKS_LOADED = true; } catch (e) { WORKS_ERR = e.message || String(e); WORKS_LOADED = true; } } // ============================================================ // Whoami data — loaded from whoami.json // ============================================================ let WHOAMI = null; let WHOAMI_LOADED = false; let WHOAMI_ERR = null; async function loadWhoami() { try { const res = await fetch("whoami.json", { cache: "no-cache" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); WHOAMI = await res.json(); WHOAMI_LOADED = true; } catch (e) { WHOAMI_ERR = e.message || String(e); WHOAMI_LOADED = true; } } // ============================================================ // Blogs index — loaded from blogs.json // each entry: { id, title, file, date? } // `file` is resolved relative to the blogs/ folder // ============================================================ let BLOGS = []; let BLOGS_LOADED = false; let BLOGS_ERR = null; const BLOGS_PER_PAGE = 10; async function loadBlogs() { try { const res = await fetch("blogs.json", { cache: "no-cache" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const raw = await res.json(); // newest first; entries without a parseable date sink to the bottom BLOGS = raw.slice().sort((a, b) => { const ta = a.date ? Date.parse(a.date) : NaN; const tb = b.date ? Date.parse(b.date) : NaN; const va = Number.isNaN(ta) ? -Infinity : ta; const vb = Number.isNaN(tb) ? -Infinity : tb; return vb - va; }); BLOGS_LOADED = true; } catch (e) { BLOGS_ERR = e.message || String(e); BLOGS_LOADED = true; } } // ============================================================ // Tiny markdown renderer — terminal flavored // supports: # h1 / ## h2 / ### h3 // **bold**, *italic*, `code` // - / * unordered list // 1. ordered list // ```fenced code blocks``` // > blockquote // [text](url) // | tables | // ============================================================ function escapeHTML(s){ return s.replace(/&/g,"&").replace(//g,">"); } function inlineMD(s){ s = escapeHTML(s); // inline code s = s.replace(/`([^`]+)`/g, '$1'); // bold s = s.replace(/\*\*([^*]+)\*\*/g, '$1'); // italic (single *) s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1$2'); // links [text](url) — in a terminal we just show text + (url) dimmed s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)'); return s; } function renderMarkdown(md){ const lines = md.split(/\r?\n/); const out = []; let i = 0; while (i < lines.length){ const line = lines[i]; // fenced code if (/^```/.test(line)){ const lang = line.replace(/^```/,"").trim(); const buf = []; i++; while (i < lines.length && !/^```/.test(lines[i])){ buf.push(escapeHTML(lines[i])); i++; } i++; // closing fence out.push(`
${buf.join("\n")}
`); continue; } // headings let m; if ((m = line.match(/^###\s+(.*)$/))){ out.push(`
${inlineMD(m[1])}
`); i++; continue; } if ((m = line.match(/^##\s+(.*)$/))) { out.push(`
${inlineMD(m[1])}
`); i++; continue; } if ((m = line.match(/^#\s+(.*)$/))) { out.push(`
${inlineMD(m[1])}
`); i++; continue; } // blockquote if (/^>\s?/.test(line)){ const buf = []; while (i < lines.length && /^>\s?/.test(lines[i])){ buf.push(inlineMD(lines[i].replace(/^>\s?/, ""))); i++; } out.push(`
${buf.join("
")}
`); continue; } // unordered list if (/^[-*]\s+/.test(line)){ const buf = []; while (i < lines.length && /^[-*]\s+/.test(lines[i])){ buf.push(`
• ${inlineMD(lines[i].replace(/^[-*]\s+/, ""))}
`); i++; } out.push(buf.join("")); continue; } // ordered list if (/^\d+\.\s+/.test(line)){ const buf = []; let n = 1; while (i < lines.length && /^\d+\.\s+/.test(lines[i])){ buf.push(`
${n}. ${inlineMD(lines[i].replace(/^\d+\.\s+/, ""))}
`); n++; i++; } out.push(buf.join("")); continue; } // table if (/^\|.*\|\s*$/.test(line) && i+1 < lines.length && /^\|[\s\-:|]+\|\s*$/.test(lines[i+1])){ const header = line.split("|").slice(1,-1).map(c => c.trim()); i += 2; const rows = []; while (i < lines.length && /^\|.*\|\s*$/.test(lines[i])){ rows.push(lines[i].split("|").slice(1,-1).map(c => c.trim())); i++; } const widths = header.map((h, ci) => Math.max(h.length, ...rows.map(r => (r[ci]||"").length)) ); const fmt = (cells) => " " + cells.map((c, ci) => (c||"").padEnd(widths[ci], " ")).join(" │ "); const sep = " " + widths.map(w => "─".repeat(w)).join("──┼──"); const lines2 = [ `
${escapeHTML(fmt(header))}
`, `
${escapeHTML(sep)}
`, ...rows.map(r => `
${escapeHTML(fmt(r))}
`), ]; out.push(lines2.join("")); continue; } // blank if (line.trim() === ""){ out.push(`
 
`); i++; continue; } // paragraph out.push(`
${inlineMD(line)}
`); i++; } return out.join(""); } // ============================================================ // ASCII art // ============================================================ const SKULL = String.raw` _________ / ======= \ / __________\ | ___________ | | | - | | | | | | | |_________| |____________________ \=____________/ Yuhan Lee (lxb) ) / """"""""""" \ / / ::::::::::::: \ =D-' (_________________) `.trimEnd(); const NEOFETCH_SKULL = String.raw` |*\_/*|________ ||_/-\_|______ | | | | | | | 0 0 | | | | - | | | | \___/ | | | |___________| | |_______________| _|________|_ / ********** \ / ************ \ -------------------- `.trimEnd(); // ============================================================ // Filesystem mock // ============================================================ const FS = { files: [ { name: ".bashrc", type: "f", size: "1.2K", date: "Apr 14" }, { name: ".config/", type: "d", size: "4.0K", date: "Apr 22" }, { name: ".ssh/", type: "d", size: "4.0K", date: "Mar 03" }, { name: "Documents/", type: "d", size: "4.0K", date: "Apr 26" }, { name: "exploits/", type: "d", size: "4.0K", date: "Apr 27" }, { name: "manifesto.txt", type: "f", size: " 847", date: "Apr 27" }, { name: "mycat.png", type: "f", size: "2.1K", date: "Apr 25" }, { name: "rice.conf", type: "f", size: " 312", date: "Apr 19" }, { name: "todo.md", type: "f", size: " 42", date: "Apr 27" }, ] }; // ============================================================ // Boot / greet messages // ============================================================ const BOOT = [ { t: 0, text: "[ 0.000000] vintage-tty v3.14 :: handshake established" }, { t: 90, text: "[ 0.014221] decrypting payload ░░░░░░░░░░ OK" }, { t: 180, text: "[ 0.041337] mounting /dev/null over /opt/reality" }, { t: 280, text: "[ 0.063402] you are not supposed to be here." }, { t: 380, text: "[ 0.072115] the others are watching the others." }, // { t: 480, text: "" }, { t: 520, text: SKULL, cls: "hot" }, { t: 560, text: "" }, { t: 600, text: " >> the lights flicker because we are.", cls: "dim" }, { t: 700, text: " >> every keystroke is a small confession.", cls: "dim" }, { t: 800, text: " >> type `help` if you forgot the words.", cls: "dim" }, { t: 900, text: "" }, ]; // ============================================================ // Commands // ============================================================ const SUDO_QUOTES = [ "Nice try.", "lxb is not in the sudoers file. This incident will be reported.", "Permission denied. Have you tried asking nicely?", "[sudo] password for lxb: (just kidding, it's not that easy)", "ERROR: insufficient privilege. insufficient nerve.", ]; function neofetchOutput() { const lines = [ { k: "OS", v: "Fedora Linux 42 (Workstation Edition) x86_64" }, { k: "Host", v: "lixiaobai@fedora" }, { k: "Kernel", v: "Linux 6.19.11-100.fc42.x86_64" }, { k: "Uptime", v: "2 hours, 24 mins" }, { k: "Packages", v: "7821 (rpm), 46 (flatpak)" }, { k: "Shell", v: "zsh 5.9" }, { k: "WM", v: "niri (Wayland)" }, { k: "Terminal", v: "claude" }, { k: "CPU", v: "13th Gen Intel(R) Core(TM) i7-13700H (20) @ 5.00 GHz" }, { k: "GPU", v: "NVIDIA GeForce RTX 4060 Max-Q / Mobile [Discrete]" }, { k: "Memory", v: "4.87 GiB / 15.40 GiB (32%)" }, { k: "Disk", v: "297.30 GiB / 952.28 GiB (31%) [btrfs]" }, { k: "Local IP", v: "172.20.10.3/28 (wlp0s20f3)" }, { k: "Locale", v: "en_US.UTF-8" }, ]; return { kind: "neofetch", lines }; } function helpOutput() { const cmds = [ ["neofetch", "get system info"], ["whoami", "about me"], ["works", "list my works / projects"], ["work ", "show details for one work"], ["blogs [page]", "list blog posts (10 per page)"], ["blog ", "read a blog post"], ["ls", "list files in $HOME"], // ["mycat", "display photo of my cat to stdout"], ["date", "current local date/time"], ["themes", "list available phosphor themes"], ["theme ", "switch theme (e.g. theme dec-vt220)"], ["sudo ", "elevate. (good luck.)"], ["clear", "wipe the screen"], ["help", "you are reading it"], ]; return { kind: "lines", lines: [ { text: "available commands :: ", cls: "dim" }, { text: "" }, ...cmds.map(([c, d]) => ({ text: ` ${c.padEnd(14, " ")} ${d}`, })), { text: "" }, { text: "↑/↓ scroll command history.", cls: "dim" }, ], }; } function lsOutput(args) { const long = args.includes("-l") || args.includes("-la") || args.includes("-al"); const all = args.includes("-a") || args.includes("-la") || args.includes("-al"); const items = FS.files.filter(f => all || !f.name.startsWith(".")); if (long) { return { kind: "lines", lines: [ { text: `total ${items.length * 4}`, cls: "dim" }, ...items.map(f => ({ text: `${f.type === "d" ? "drwxr-xr-x" : "-rw-r--r--"} 1 lxb lxb ${f.size.padStart(5," ")} ${f.date} ${f.name}`, cls: f.type === "d" ? "hot" : "", })), ], }; } // grid layout const names = items.map(f => f.type === "d" ? f.name : f.name); const cols = 4; const colWidth = Math.max(...names.map(n => n.length)) + 2; const rows = []; for (let i = 0; i < names.length; i += cols) { rows.push(names.slice(i, i + cols).map(n => n.padEnd(colWidth, " ")).join("")); } return { kind: "lines", lines: rows.map(r => ({ text: r })) }; } function dateOutput() { const d = new Date(); const opts = { weekday:"short", year:"numeric", month:"short", day:"2-digit" }; const s = d.toLocaleDateString("en-US", opts) + " " + d.toLocaleTimeString("en-GB") + " CRT-TZ"; return { kind: "lines", lines: [{ text: s }] }; } function themesOutput() { return { kind: "themes" }; } // ============================================================ // Works commands // ============================================================ function worksOutput() { if (!WORKS_LOADED) return { kind: "lines", lines: [{ text: "works: still loading…", cls: "dim" }] }; if (WORKS_ERR) return { kind: "lines", lines: [ { text: `works: failed to load works.json (${WORKS_ERR})` }, { text: `serve this page over http(s) — file:// blocks fetch.`, cls: "dim" }, ]}; if (WORKS.length === 0) return { kind: "lines", lines: [{ text: "no works yet. add some to works.json.", cls: "dim" }] }; return { kind: "works-list" }; } function workOutput(args){ if (!WORKS_LOADED) return { kind: "lines", lines: [{ text: "works: still loading…", cls: "dim" }] }; if (WORKS_ERR) return { kind: "lines", lines: [{ text: `works: ${WORKS_ERR}` }]}; if (args.length === 0){ return { kind: "lines", lines: [ { text: "usage: work ", cls: "dim" }, { text: "(see `works` for the list)", cls: "dim" }, ]}; } const id = args[0].toLowerCase(); const w = WORKS.find(x => x.id.toLowerCase() === id); if (!w){ return { kind: "lines", lines: [ { text: `work: not found: '${id}'` }, { text: "try `works` to see ids.", cls: "dim" }, ]}; } return { kind: "work-detail", work: w }; } // ============================================================ // Blogs commands // ============================================================ function blogsOutput(args) { if (!BLOGS_LOADED) return { kind: "lines", lines: [{ text: "blogs: still loading…", cls: "dim" }] }; if (BLOGS_ERR) return { kind: "lines", lines: [ { text: `blogs: failed to load blogs.json (${BLOGS_ERR})` }, { text: "is the server running from the project root?", cls: "dim" }, ]}; if (BLOGS.length === 0) return { kind: "lines", lines: [{ text: "no blogs yet.", cls: "dim" }] }; const totalPages = Math.max(1, Math.ceil(BLOGS.length / BLOGS_PER_PAGE)); let page = 1; if (args.length > 0) { const n = parseInt(args[0], 10); if (Number.isNaN(n) || n < 1) { return { kind: "lines", lines: [ { text: `blogs: invalid page '${args[0]}'` }, { text: `usage: blogs [page] (1 .. ${totalPages})`, cls: "dim" }, ]}; } if (n > totalPages) { return { kind: "lines", lines: [ { text: `blogs: page ${n} out of range (only ${totalPages} page${totalPages === 1 ? "" : "s"})` }, ]}; } page = n; } return { kind: "blogs-list", page, totalPages }; } function blogOutput(args){ if (!BLOGS_LOADED) return { kind: "lines", lines: [{ text: "blogs: still loading…", cls: "dim" }] }; if (BLOGS_ERR) return { kind: "lines", lines: [{ text: `blogs: ${BLOGS_ERR}` }]}; if (args.length === 0){ return { kind: "lines", lines: [ { text: "usage: blog ", cls: "dim" }, { text: "(see `blogs` for the list)", cls: "dim" }, ]}; } const id = args[0].toLowerCase(); const b = BLOGS.find(x => x.id.toLowerCase() === id); if (!b){ return { kind: "lines", lines: [ { text: `blog: not found: '${id}'` }, { text: "try `blogs` to see ids.", cls: "dim" }, ]}; } return { kind: "blog-detail", blog: b }; } function themeOutput(args) { if (args.length === 0) { const cur = getTheme(); return { kind: "lines", lines: [ { text: `current theme: ${cur}` }, { text: `usage: theme (try \`themes\` to list)`, cls: "dim" }, ], }; } const target = args[0].toLowerCase(); const found = THEMES.find(t => t.id === target); if (!found) { // soft-match common shortcuts const aliases = { amber: "ibm-5151", ibm: "ibm-5151", green: "dec-vt220", vt220: "dec-vt220", dec: "dec-vt220", blue: "commodore-64", c64: "commodore-64", commodore: "commodore-64", white: "hercules", herc: "hercules", apple: "apple-ii", ii: "apple-ii", orange: "plasma", red: "plasma", }; const aliased = aliases[target]; if (aliased) { const t = THEMES.find(x => x.id === aliased); setTheme(t.id); return { kind: "lines", lines: [ { text: `theme set: ${t.name} (${t.id}) ── ${t.desc}` }, ], }; } return { kind: "lines", lines: [ { text: `theme: unknown phosphor: '${target}'`, cls: "" }, { text: `try \`themes\` to list available displays.`, cls: "dim" }, ], }; } setTheme(found.id); return { kind: "lines", lines: [ { text: `theme set: ${found.name} (${found.id}) ── ${found.desc}` }, ], }; } function whoamiOutput() { if (!WHOAMI_LOADED) return { kind: "lines", lines: [{ text: "whoami: still loading…", cls: "dim" }] }; if (WHOAMI_ERR) return { kind: "lines", lines: [ { text: `whoami: failed to load whoami.json (${WHOAMI_ERR})` }, { text: "is the server running from the project root?", cls: "dim" }, ]}; if (!WHOAMI) return { kind: "lines", lines: [{ text: "whoami: empty whoami.json.", cls: "dim" }] }; return { kind: "whoami", who: WHOAMI }; } function mycatOutput() { return { kind: "image", src: "mycat.png", caption: "mycat.png -- 327x247 -- phosphor-encoded", }; } function sudoOutput(rest) { const q = SUDO_QUOTES[Math.floor(Math.random() * SUDO_QUOTES.length)]; const cmd = rest.join(" "); return { kind: "lines", lines: [ { text: cmd ? `sudo: attempting :: ${cmd}` : "sudo: missing operand", cls: "dim" }, { text: q }, ], }; } function unknownOutput(cmd) { return { kind: "lines", lines: [ { text: `bash: ${cmd}: command not found` }, { text: `did you mean: help?`, cls: "dim" }, ], }; } function runCommand(raw) { const trimmed = raw.trim(); if (!trimmed) return null; const parts = trimmed.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); switch (cmd) { case "neofetch": return neofetchOutput(); case "whoami": return whoamiOutput(); case "ls": return lsOutput(args); case "help": return helpOutput(); case "date": return dateOutput(); // case "mycat": return mycatOutput(); case "themes": return themesOutput(); case "theme": return themeOutput(args); case "works": return worksOutput(); case "work": return workOutput(args); case "blogs": return blogsOutput(args); case "blog": return blogOutput(args); case "sudo": return sudoOutput(args); case "clear": return { kind: "clear" }; case "exit": case "logout": return { kind: "lines", lines: [{ text: "you can check out any time you like.", cls: "dim" }] }; default: return unknownOutput(cmd); } } // ============================================================ // Output renderers // ============================================================ function NeofetchBlock({ lines }) { const ascii = NEOFETCH_SKULL; return (
{ascii}
lxb@fedora
────────────
{lines.map((l, i) => (
{l.k} : {l.v}
))}
colors:
{[0.25, 0.4, 0.55, 0.7, 0.85, 1.0].map((a, i) => ( ))}
); } function ImageBlock({ src, caption }) { return (
{caption}
{caption}
); } function LinesBlock({ lines }) { return ( <> {lines.map((l, i) => (
{l.text === "" ? "\u00A0" : (l.text.includes("\n") ?
{l.text}
: l.text)}
))} ); } function ThemesBlock() { const cur = getTheme(); return (
available phosphor themes — type `theme <id>` to switch
 
{THEMES.map(t => (
{t.id}
[{t.year}]
{t.name}
{t.desc}
))}
); } function WorksListBlock() { return (
my works — type `work <id>` for details
 
{WORKS.map(w => (
{w.id}
{w.title}
{w.description}
))}
 
{WORKS.length} work{WORKS.length === 1 ? "" : "s"} indexed.
); } function WhoamiBlock({ who }) { const html = useMemo(() => renderMarkdown(who.descriptions || ""), [who]); return (
{who.avatar ? (
{who.name} { e.currentTarget.style.display = "none"; }} onLoad={() => { const term = document.querySelector(".term"); if (term) term.scrollTop = term.scrollHeight; }} />
) : null}
{who.name}
 
); } function BlogsListBlock({ page, totalPages }) { const start = (page - 1) * BLOGS_PER_PAGE; const slice = BLOGS.slice(start, start + BLOGS_PER_PAGE); return (
my blogs — type `blog <id>` to read
 
{slice.map(b => (
{b.id}
{b.title}
{b.date || ""}
))}
 
page {page} / {totalPages} ── {BLOGS.length} entr{BLOGS.length === 1 ? "y" : "ies"} indexed {totalPages > 1 ? ` ── next: \`blogs ${page < totalPages ? page + 1 : 1}\`` : ""}
); } function BlogDetailBlock({ blog }) { const [md, setMd] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let cancelled = false; fetch(`blogs/${blog.file}`, { cache: "no-cache" }) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.text(); }) .then(t => { if (cancelled) return; setMd(t); // markdown landed after layout — re-pin the cmd line to the top const term = document.querySelector(".term"); if (!term) return; const cmds = term.querySelectorAll(".cmd-line"); const cmdEl = cmds[cmds.length - 1]; if (cmdEl) { const top = cmdEl.getBoundingClientRect().top - term.getBoundingClientRect().top + term.scrollTop; term.scrollTop = top; } }) .catch(e => { if (!cancelled) setErr(e.message || String(e)); }); return () => { cancelled = true; }; }, [blog]); const html = useMemo(() => (md ? renderMarkdown(md) : ""), [md]); return (
┌─ {blog.title} [{blog.id}]
{blog.date ?
└─ {blog.date}
: null}
 
{err ? (
blog: failed to load ({err})
) : md == null ? (
loading…
) : (
)}
); } function WorkDetailBlock({ work }) { const html = useMemo(() => renderMarkdown(work.detail || ""), [work]); return (
┌─ {work.title} [{work.id}]
└─ {work.description}
 
{work.image ? ( <>
{work.title} { e.currentTarget.style.display = "none"; }} />
{work.image}
 
) : null}
); } // Render boot lines that include the multi-line skull function BootLine({ text, cls }) { if (text.includes("\n")) { return
{text}
; } return
{text === "" ? "\u00A0" : text}
; } // ============================================================ // Prompt — old-school bash style: [user@host pwd]$ // ============================================================ function Prompt({ path = "~" }) { return ( {"["} lxb@fedora {path} {"]$ "} ); } // ============================================================ // Main terminal // ============================================================ function Terminal() { // history of [{type:'boot'|'cmd'|'output', ...}] const [items, setItems] = useState([]); const [input, setInput] = useState(""); const [caret, setCaret] = useState(0); const [cmdHistory, setCmdHistory] = useState([]); const [histIdx, setHistIdx] = useState(-1); // -1 means "live input" const [bootDone, setBootDone] = useState(false); const [focused, setFocused] = useState(true); const inputRef = useRef(null); const termRef = useRef(null); // Boot sequence useEffect(() => { loadWorks(); loadWhoami(); loadBlogs(); const timers = []; BOOT.forEach((b, i) => { timers.push(setTimeout(() => { setItems(prev => [...prev, { type: "boot", text: b.text, cls: b.cls }]); if (i === BOOT.length - 1) setBootDone(true); }, b.t)); }); return () => timers.forEach(clearTimeout); }, []); // Auto-scroll on new items: long-form outputs (work/blog detail) pin their // cmd line to the top so markdown can be read from the start; everything // else snaps to bottom. const isPinned = (it) => it && it.type === "output" && it.payload && (it.payload.kind === "work-detail" || it.payload.kind === "blog-detail"); useEffect(() => { if (!termRef.current) return; const term = termRef.current; const last = items[items.length - 1]; if (isPinned(last)) { const cmds = term.querySelectorAll(".cmd-line"); const cmdEl = cmds[cmds.length - 1]; if (cmdEl) { const top = cmdEl.getBoundingClientRect().top - term.getBoundingClientRect().top + term.scrollTop; term.scrollTop = top; } return; } term.scrollTop = term.scrollHeight; }, [items]); // While typing, follow the bottom — but never override a pinned long-form output. useEffect(() => { if (!termRef.current) return; if (isPinned(items[items.length - 1])) return; termRef.current.scrollTop = termRef.current.scrollHeight; }, [input]); // Keep focus const refocus = useCallback(() => { if (inputRef.current) inputRef.current.focus(); }, []); useEffect(() => { refocus(); }, [refocus, bootDone]); useEffect(() => { const onClick = () => refocus(); document.addEventListener("click", onClick); return () => document.removeEventListener("click", onClick); }, [refocus]); const submit = (raw) => { const result = runCommand(raw); setItems(prev => { const next = [...prev, { type: "cmd", text: raw }]; if (!result) return next; if (result.kind === "clear") return []; return [...next, { type: "output", payload: result }]; }); if (raw.trim()) { setCmdHistory(prev => [...prev, raw]); } setHistIdx(-1); setInput(""); setCaret(0); }; const recallHistory = (val) => { setInput(val); setCaret(val.length); }; const onKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); submit(input); } else if (e.key === "ArrowUp") { e.preventDefault(); if (cmdHistory.length === 0) return; const newIdx = histIdx === -1 ? cmdHistory.length - 1 : Math.max(0, histIdx - 1); setHistIdx(newIdx); recallHistory(cmdHistory[newIdx]); } else if (e.key === "ArrowDown") { e.preventDefault(); if (histIdx === -1) return; const newIdx = histIdx + 1; if (newIdx >= cmdHistory.length) { setHistIdx(-1); recallHistory(""); } else { setHistIdx(newIdx); recallHistory(cmdHistory[newIdx]); } } else if (e.key === "l" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setItems([]); } else if (e.key === "c" && e.ctrlKey) { e.preventDefault(); setItems(prev => [...prev, { type: "cmd", text: input + "^C" }]); setInput(""); setCaret(0); setHistIdx(-1); } }; const syncCaret = (e) => { const pos = e.target.selectionStart; if (pos != null) setCaret(pos); }; return ( <>
{items.map((it, i) => { if (it.type === "boot") { return ; } if (it.type === "cmd") { return (
{it.text}
); } if (it.type === "output") { const p = it.payload; if (p.kind === "neofetch") return ; if (p.kind === "image") return ; if (p.kind === "lines") return ; if (p.kind === "themes") return ; if (p.kind === "works-list") return ; if (p.kind === "work-detail") return ; if (p.kind === "whoami") return ; if (p.kind === "blogs-list") return ; if (p.kind === "blog-detail") return ; } return null; })} {/* live prompt */} {bootDone && (
{input.slice(0, caret)} {input.slice(caret)} { setInput(e.target.value); setCaret(e.target.selectionStart ?? e.target.value.length); setHistIdx(-1); }} onKeyDown={onKeyDown} onKeyUp={syncCaret} onClick={syncCaret} onSelect={syncCaret} onFocus={(e) => { setFocused(true); syncCaret(e); }} onBlur={() => setFocused(false)} />
)}
); } ReactDOM.createRoot(document.getElementById("root")).render();