/* 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 (
);
}
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 ? (

{ 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 ? (
<>

{ 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 (
);
}
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 && (
)}
>
);
}
ReactDOM.createRoot(document.getElementById("root")).render();