/* +------------------+----------------------------------------------------------+ | FILE | tui.tsx | | ROLE | Plan 9 / Acme-inspired TUI content browser | | OWNER | Kris Yotam | | CREATED | 2026-02-28 | | UPDATED | 2026-02-28 | +------------------+----------------------------------------------------------+ | @type component | | @path src/components/core/tui.tsx | +------------------+----------------------------------------------------------+ | SUMMARY | | Full Plan 9 desktop layout: teal desktop background with island sections. | | 3 top bars, central Acme content browser, footer taskbar — all floating | | with gaps between them. Draggable Cirno mascot with minimize. | | Left pane: collapsible directory tree. Right pane: raw MDX viewer. | | Keyboard navigation (arrows, Enter, Escape) and search. | +-----------------------------------------------------------------------------+ */ "use client" import { useState, useCallback, useEffect, useRef, useMemo } from "react" import { ScrollArea } from "@/components/ui/scroll-area" // Types interface TreeEntry { slug: string title: string date: string preview: string } interface TUIProps { tree: Record } interface SelectedFile { type: string slug: string } interface FileContent { frontmatter: string body: string } // Plan 9 Color Palette const P9 = { desktop: "#B5B5AD", // grey desktop background tagBar: "#EAFFFF", // pale cyan — Acme tag/command bar titleBar: "#FFFFEA", // pale yellow — Acme window title bars body: "#FFFFF5", // cream — Acme body/content background text: "#000000", textMuted: "#555555", selected: "#9EE09E", border: "#9EEEEE", // Plan 9 teal blue window borders borderInner: "#888888", // inner dividers (pane separators) borderLight: "#9EEEEE", hoverBg: "#EEEEE5", barAccent1: "#98D1CB", barAccent2: "#E8A0A0", barAccent3: "#88CC88", footerBg: "#9EDBDA", wsGrid: "#EAFFFF", wsHighlight: "#55CCCC", } as const const GAP = "14px" // SVG Icons (Plan 9 pixel-art style, no emoji) function FolderIcon({ size = 12 }: { size?: number }) { return ( ) } function HomeIcon({ size = 12 }: { size?: number }) { return ( ) } // Floating Cirno function Cirno({ minimized, setMinimized }: { minimized: boolean; setMinimized: (v: boolean) => void }) { const [pos, setPos] = useState(() => { if (typeof window !== "undefined") { return { x: Math.round((window.innerWidth - 560) / 2), y: Math.round((window.innerHeight - 600) / 2), } } return { x: 200, y: 100 } }) const [dragging, setDragging] = useState(false) const dragOffset = useRef({ x: 0, y: 0 }) const onMouseDown = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement if (target.closest("[data-cirno-close]")) return e.preventDefault() setDragging(true) dragOffset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y, } }, [pos]) useEffect(() => { if (!dragging) return function onMove(e: MouseEvent) { setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y, }) } function onUp() { setDragging(false) } window.addEventListener("mousemove", onMove) window.addEventListener("mouseup", onUp) return () => { window.removeEventListener("mousemove", onMove) window.removeEventListener("mouseup", onUp) } }, [dragging]) if (minimized) { return null } return (
/dev/cirno
Cirno
) } // Top Bars (3 separate island bars) function TopBars({ totalFiles }: { totalFiles: number }) { const now = new Date() const timeStr = now.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }) return (
{/* Bar 1: Status bar */}
krisyotam(9) front
{/* Plan 9 stats graph — 4 rows spelling KRIS */}
{[ { letter: "K", color: "#CC4444", bar1: "55%", bar2: "30%" }, { letter: "R", color: "#4444BB", bar1: "70%", bar2: "45%" }, { letter: "I", color: "#228822", bar1: "35%", bar2: "80%" }, { letter: "S", color: "#77BB55", bar1: "60%", bar2: "20%" }, ].map((row, i) => (
{/* Small square with letter */}
{row.letter}
{/* Bar 1 */}
{/* Vertical divider */}
{/* Bar 2 */}
))}
{/* Bar 2: File browser (Plan 9 file manager style) */}
{/* Title bar: home, folder, up arrow | /usr/krisyotam | new folder, new file */}
/usr/krisyotam
{/* Directory listing */}
Blog 0 Feb 28 10:15
Books 0 Mar 14 23:30
Documents 0 Jun 27 23:43
{/* Bar 3: Navigation grid — command menu items */}
navigate(1) site
{[ { label: "home", path: "/" }, { label: "essays", path: "/essays" }, { label: "blog", path: "/blog" }, { label: "diary", path: "/diary" }, { label: "reviews", path: "/reviews" }, { label: "fiction", path: "/fiction" }, { label: "verse", path: "/verse" }, { label: "til", path: "/til" }, { label: "now", path: "/now" }, { label: "film", path: "/film" }, { label: "anime", path: "/anime" }, { label: "manga", path: "/manga" }, { label: "reading", path: "/reading" }, { label: "library", path: "/library" }, { label: "tags", path: "/tags" }, { label: "about", path: "/me" }, { label: "stats", path: "/stats" }, { label: "globe", path: "/globe" }, { label: "contact", path: "/contact" }, ].map((item) => ( { e.currentTarget.style.background = P9.wsHighlight e.currentTarget.style.color = "#fff" }} onMouseLeave={(e) => { e.currentTarget.style.background = "#D8D8D0" e.currentTarget.style.color = P9.text }} style={{ padding: "1px 3px", textAlign: "center", background: "#D8D8D0", color: P9.text, border: "1px solid #FFFFFF", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "inherit", fontSize: "inherit", }} > {item.label} ))}
) } // Footer Taskbar function Footer({ cirnoMinimized, onRestoreCirno }: { cirnoMinimized: boolean; onRestoreCirno: () => void }) { const [time, setTime] = useState("") useEffect(() => { function tick() { const d = new Date() const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] setTime(`${days[d.getDay()]} ${months[d.getMonth()]} ${d.getDate()} ${d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" })}`) } tick() const id = setInterval(tick, 10000) return () => clearInterval(id) }, []) return (
{/* Home button */} /home {/* Collapsed cirno.jpg — Plan 9 collapsed window style */} {cirnoMinimized && (
cirno.jpg
)} {/* Colored center panel */}
{/* Clock + info */}
{time} krisyotam.com 9front
) } // Main TUI Component export function TUI({ tree }: TUIProps) { const [expandedDirs, setExpandedDirs] = useState>(new Set()) const [selectedFile, setSelectedFile] = useState(null) const [fileContent, setFileContent] = useState(null) const [searchQuery, setSearchQuery] = useState("") const [loading, setLoading] = useState(false) const [focusIndex, setFocusIndex] = useState(-1) const [cirnoMinimized, setCirnoMinimized] = useState(false) const treeRef = useRef(null) const contentTypes = useMemo(() => Object.keys(tree).sort(), [tree]) const totalFiles = useMemo( () => Object.values(tree).reduce((sum, entries) => sum + entries.length, 0), [tree] ) const navItems = useMemo(() => { const items: { kind: "dir" | "file"; type: string; slug?: string }[] = [] const query = searchQuery.toLowerCase() for (const type of contentTypes) { const entries = tree[type] const filtered = query ? entries.filter( (e) => e.slug.toLowerCase().includes(query) || e.title.toLowerCase().includes(query) ) : entries if (query && filtered.length === 0) continue items.push({ kind: "dir", type }) if (expandedDirs.has(type)) { for (const entry of filtered) { items.push({ kind: "file", type, slug: entry.slug }) } } } return items }, [contentTypes, tree, expandedDirs, searchQuery]) const filteredTree = useMemo(() => { if (!searchQuery) return tree const query = searchQuery.toLowerCase() const result: Record = {} for (const type of contentTypes) { const filtered = tree[type].filter( (e) => e.slug.toLowerCase().includes(query) || e.title.toLowerCase().includes(query) ) if (filtered.length > 0) result[type] = filtered } return result }, [tree, searchQuery, contentTypes]) const catFile = useCallback(async (type: string, slug: string) => { setSelectedFile({ type, slug }) setLoading(true) setFileContent(null) try { const res = await fetch(`/api/tui/${type}/${slug}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setFileContent({ frontmatter: data.frontmatter, body: data.body }) } catch { setFileContent({ frontmatter: "", body: `Error: failed to read ${type}/${slug}.mdx`, }) } finally { setLoading(false) } }, []) const toggleDir = useCallback((type: string) => { setExpandedDirs((prev) => { const next = new Set(prev) if (next.has(type)) next.delete(type) else next.add(type) return next }) }, []) useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if ((e.target as HTMLElement).tagName === "INPUT") return switch (e.key) { case "ArrowDown": e.preventDefault() setFocusIndex((prev) => Math.min(prev + 1, navItems.length - 1)) break case "ArrowUp": e.preventDefault() setFocusIndex((prev) => Math.max(prev - 1, 0)) break case "ArrowRight": { e.preventDefault() const item = navItems[focusIndex] if (item?.kind === "dir" && !expandedDirs.has(item.type)) { toggleDir(item.type) } break } case "ArrowLeft": { e.preventDefault() const item = navItems[focusIndex] if (item?.kind === "dir" && expandedDirs.has(item.type)) { toggleDir(item.type) } else if (item?.kind === "file") { const dirIdx = navItems.findIndex( (n) => n.kind === "dir" && n.type === item.type ) if (dirIdx >= 0) setFocusIndex(dirIdx) } break } case "Enter": { e.preventDefault() const item = navItems[focusIndex] if (!item) break if (item.kind === "dir") toggleDir(item.type) else if (item.slug) catFile(item.type, item.slug) break } case "Escape": e.preventDefault() setSelectedFile(null) setFileContent(null) break } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) }, [navItems, focusIndex, expandedDirs, toggleDir, catFile]) useEffect(() => { if (focusIndex < 0 || !treeRef.current) return const el = treeRef.current.querySelector(`[data-nav-index="${focusIndex}"]`) el?.scrollIntoView({ block: "nearest" }) }, [focusIndex]) const breadcrumb = selectedFile ? `/content/${selectedFile.type}/${selectedFile.slug}` : "/content" // Render return (
{/* Floating Cirno */} {/* ================================================================== */} {/* Top 3 Bars (separate islands) */} {/* ================================================================== */} {/* ================================================================== */} {/* Central Island: Acme workspace */} {/* ================================================================== */}
{/* Acme command/tag bar */}
New Cut Paste Snarf Sort Zerox Delcol | { setSearchQuery(e.target.value) setFocusIndex(-1) }} style={{ background: P9.body, border: `1px solid ${P9.borderInner}`, padding: "1px 6px", fontFamily: "inherit", fontSize: "inherit", width: "180px", outline: "none", }} />
{/* Panes */}
{/* Left Pane — Directory Tree */}
{breadcrumb} {totalFiles}
Del Snarf | Look Put Mail
{contentTypes.map((type) => { const entries = filteredTree[type] if (!entries && searchQuery) return null const isExpanded = expandedDirs.has(type) const fileCount = tree[type].length const dirNavIdx = navItems.findIndex( (n) => n.kind === "dir" && n.type === type ) return (
toggleDir(type)} style={{ padding: "1px 8px", cursor: "pointer", display: "flex", alignItems: "center", gap: "4px", background: focusIndex === dirNavIdx ? P9.selected : "transparent", userSelect: "none", }} onMouseEnter={(e) => { if (focusIndex !== dirNavIdx) e.currentTarget.style.background = P9.hoverBg }} onMouseLeave={(e) => { if (focusIndex !== dirNavIdx) e.currentTarget.style.background = "transparent" }} > {isExpanded ? "\u25BE" : "\u25B8"} {type}/ {fileCount}
{isExpanded && (entries || tree[type]).map((entry) => { const fileNavIdx = navItems.findIndex( (n) => n.kind === "file" && n.type === type && n.slug === entry.slug ) const isSelected = selectedFile?.type === type && selectedFile?.slug === entry.slug return (
catFile(type, entry.slug)} title={`${entry.title}\n${entry.date}\n${entry.preview}`} style={{ padding: "1px 8px 1px 28px", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", background: isSelected ? P9.selected : focusIndex === fileNavIdx ? P9.selected : "transparent", userSelect: "none", }} onMouseEnter={(e) => { if (!isSelected && focusIndex !== fileNavIdx) e.currentTarget.style.background = P9.hoverBg }} onMouseLeave={(e) => { if (!isSelected && focusIndex !== fileNavIdx) e.currentTarget.style.background = "transparent" }} > {entry.slug}.mdx
) })}
) })}
{/* Right Pane — Content Viewer */}
{selectedFile ? `${selectedFile.type}/${selectedFile.slug}.mdx` : "output"} Del Snarf | Look Put
{selectedFile ? `$ cat ${selectedFile.type}/${selectedFile.slug}.mdx` : "cat"}
{!selectedFile && !loading && (
; Plan 9 / Acme content browser
; click a file to cat it, or use arrow keys + Enter
; Escape to clear, Look to search
;
; {contentTypes.length} directories, {totalFiles} files
)} {loading && (
reading...
)} {selectedFile && fileContent && !loading && (
{fileContent.frontmatter && ( <>
---
{fileContent.frontmatter}
---
)}
{fileContent.body}
)}
{/* ================================================================== */} {/* Footer Taskbar (separate island) */} {/* ================================================================== */}
) }