Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470// terminal.mjs — Tile-based terminal emulator for AC native// Renders a PTY process (bash, claude, etc.) through the ac-native graphics system.// Uses system.pty API to spawn and communicate with the child process.
let grid = null;let cols = 0, rows = 0;let cellW = 6, cellH = 10; // font_1 (6x10) cell sizelet started = false;let shiftHeld = false;let ctrlHeld = false;let altHeld = false;let cursorBlink = 0;let lastExitCode = -1;let lastCmd = "";
// ANSI 16-color palette → RGBconst COLORS = [ [0, 0, 0], // 0 black [170, 0, 0], // 1 red [0, 170, 0], // 2 green [170, 85, 0], // 3 yellow/brown [0, 0, 170], // 4 blue [170, 0, 170], // 5 magenta [0, 170, 170], // 6 cyan [170, 170, 170], // 7 white [85, 85, 85], // 8 bright black [255, 85, 85], // 9 bright red [85, 255, 85], // 10 bright green [255, 255, 85], // 11 bright yellow [85, 85, 255], // 12 bright blue [255, 85, 255], // 13 bright magenta [85, 255, 255], // 14 bright cyan [255, 255, 255], // 15 bright white];
const DEFAULT_FG = [170, 170, 170]; // default foreground (light grey)const DEFAULT_BG = [0, 0, 0]; // default background (black)const CELL_SIZE = 10; // elements per cell: ch, fg, bg, bold, fg_r, fg_g, fg_b, bg_r, bg_g, bg_b
function fgColor(idx, bold, r, g, b) { if (idx === 255) return [r, g, b]; // truecolor if (idx === 16) return bold ? COLORS[15] : DEFAULT_FG; // default if (idx >= 0 && idx < 8 && bold) return COLORS[idx + 8]; if (idx >= 0 && idx < 16) return COLORS[idx]; return DEFAULT_FG;}
function bgColor(idx, r, g, b) { if (idx === 255) return [r, g, b]; // truecolor if (idx === 16) return DEFAULT_BG; // default if (idx >= 0 && idx < 16) return COLORS[idx]; return DEFAULT_BG;}
// Map Unicode to ASCII replacements for our 6x10 bitmap font.// Claude Code uses heavy Unicode UI — we map everything we can.const UNICODE_MAP = { // Box drawing (light) 0x2500: "-", 0x2501: "=", 0x2502: "|", 0x2503: "|", 0x250C: "+", 0x250D: "+", 0x250E: "+", 0x250F: "+", 0x2510: "+", 0x2511: "+", 0x2512: "+", 0x2513: "+", 0x2514: "+", 0x2515: "+", 0x2516: "+", 0x2517: "+", 0x2518: "+", 0x2519: "+", 0x251A: "+", 0x251B: "+", 0x251C: "+", 0x251D: "+", 0x2523: "+", 0x2524: "+", 0x2525: "+", 0x252B: "+", 0x252C: "+", 0x2533: "+", 0x2534: "+", 0x253B: "+", 0x253C: "+", 0x254B: "+", // Box drawing (double) 0x2550: "=", 0x2551: "|", 0x2552: "+", 0x2553: "+", 0x2554: "+", 0x2555: "+", 0x2556: "+", 0x2557: "+", 0x2558: "+", 0x2559: "+", 0x255A: "+", 0x255B: "+", 0x255C: "+", 0x255D: "+", 0x255E: "+", 0x255F: "+", 0x2560: "+", 0x2561: "+", 0x2562: "+", 0x2563: "+", 0x2564: "+", 0x2565: "+", 0x2566: "+", 0x2567: "+", 0x2568: "+", 0x2569: "+", 0x256A: "+", 0x256B: "+", 0x256C: "+", // Box drawing (rounded corners) 0x256D: "+", 0x256E: "+", 0x256F: "+", 0x2570: "+", // Block elements 0x2580: "#", 0x2581: "_", 0x2582: "_", 0x2583: "_", 0x2584: "#", 0x2585: "#", 0x2586: "#", 0x2587: "#", 0x2588: "#", 0x2589: "#", 0x258A: "#", 0x258B: "#", 0x258C: "#", 0x258D: "#", 0x258E: "#", 0x258F: "#", 0x2590: "#", 0x2591: ".", 0x2592: ":", 0x2593: "#", 0x2594: "-", 0x2595: "|", // Arrows 0x2190: "<", 0x2191: "^", 0x2192: ">", 0x2193: "v", 0x2194: "<>", 0x2195: "^v", 0x2196: "\\", 0x2197: "/", 0x2198: "\\", 0x2199: "/", 0x21B5: "<-", // ↵ return 0x21E6: "<=", 0x21E8: "=>", // Bullets and symbols 0x2022: "*", 0x2023: ">", 0x25CF: "*", 0x25CB: "o", 0x25A0: "#", 0x25A1: "[]", 0x25AA: ".", 0x25AB: ".", 0x25B2: "^", 0x25B6: ">", 0x25BA: ">", 0x25BC: "v", 0x25C0: "<", 0x25C4: "<", 0x25C6: "*", 0x25C7: "*", 0x25CA: "<>", 0x25E6: "o", 0x25EF: "O", // Check marks, crosses, stars 0x2714: "+", 0x2715: "x", 0x2716: "x", 0x2718: "x", 0x2713: "+", 0x2717: "x", 0x2605: "*", 0x2606: "*", 0x2764: "<3", // Math / misc symbols 0x2026: "...", 0x00B7: ".", 0x2219: ".", 0x00D7: "x", 0x00F7: "/", 0x2260: "!=", 0x2264: "<=", 0x2265: ">=", 0x221E: "inf", 0x2248: "~=", // Spinners / braille (Claude Code progress indicators) 0x280B: "|", 0x2819: "/", 0x2838: "-", 0x2830: "\\", 0x2826: "|", 0x2807: "/", 0x280E: "-", 0x2821: "\\", // Braille patterns — map all to dots/blocks // (range 0x2800-0x28FF used by Claude spinners) // General punctuation 0x2018: "'", 0x2019: "'", 0x201C: '"', 0x201D: '"', 0x2013: "-", 0x2014: "--", 0x2015: "--", 0x2039: "<", 0x203A: ">", // Emoji shorthand (common in Claude output) 0x1F512: "[lock]", 0x1F513: "[open]", 0x1F4E6: "[pkg]", 0x1F4DD: "[note]", 0x1F680: "[go]", 0x1F50D: "[?]", 0x2728: "*", 0x26A0: "!!", 0x2699: "[*]", 0x1F916: "[bot]", 0x1F4AC: "[..]", 0x1F4C1: "[dir]", 0x1F4C4: "[doc]", 0x1F527: "[fix]", 0x1F41B: "[bug]", 0x2705: "[ok]", 0x274C: "[no]", 0x1F6D1: "[stop]", // Currency 0x00A3: "L", 0x00A5: "Y", 0x20AC: "E", // Latin extensions 0x00E9: "e", 0x00E8: "e", 0x00EA: "e", 0x00EB: "e", 0x00E0: "a", 0x00E1: "a", 0x00E2: "a", 0x00E4: "a", 0x00F2: "o", 0x00F3: "o", 0x00F4: "o", 0x00F6: "o", 0x00F9: "u", 0x00FA: "u", 0x00FB: "u", 0x00FC: "u", 0x00ED: "i", 0x00EE: "i", 0x00EF: "i", 0x00F1: "n", 0x00E7: "c", 0x00DF: "ss",};
function charReplace(ch) { if (ch >= 32 && ch < 127) return String.fromCharCode(ch); const mapped = UNICODE_MAP[ch]; if (mapped) return mapped; // Braille block (0x2800-0x28FF) — Claude spinners if (ch >= 0x2800 && ch <= 0x28FF) return "."; // Box drawing range catch-all if (ch >= 0x2500 && ch <= 0x257F) return "+"; // Block elements catch-all if (ch >= 0x2580 && ch <= 0x259F) return "#"; // Geometric shapes catch-all if (ch >= 0x25A0 && ch <= 0x25FF) return "*"; // Dingbats catch-all if (ch >= 0x2700 && ch <= 0x27BF) return "*"; // CJK / emoji — skip silently (don't show ?) if (ch >= 0x1F000) return " "; // Everything else unmapped — blank rather than ? if (ch > 127) return " "; return null;}
function boot({ system, screen, params }) { cols = Math.floor(screen.width / cellW); rows = Math.floor(screen.height / cellH);
// What to spawn — check params let cmd = "/bin/sh"; let args = []; // params come from colon-separated jump (e.g. "terminal:claude" → params=["claude"]) // Also check lastCmd global in case params didn't propagate const p0 = (params && params.length > 0 ? params[0] : null) || globalThis.__terminalCmd || null; globalThis.__terminalCmd = undefined; console.log("[terminal] params:", JSON.stringify(params), "p0:", p0, "len:", params ? params.length : "null"); if (p0 === "claude") { // Claude Code native binary // Auth: reads ANTHROPIC_API_KEY from /mnt/config.json "claude_api_key" field // If no key, Claude will show OAuth URL (QR code auto-detected in terminal) cmd = "/bin/claude"; args = ["claude"]; } else if (p0) { cmd = p0; }
lastCmd = cmd; // Persistent-session behavior: if a PTY child from a previous visit is // still running (user left the piece without killing it) and the cmd // matches what we'd spawn now, RE-ATTACH to the live session instead of // spawning a fresh one. "code → claude → esc → prompt → code" should // drop the user back into the same claude conversation mid-turn rather // than a blank restart. The PTY is owned by the runtime (not the piece) // so the child kept running while the piece was out of scope. if (system.pty.active && globalThis.__terminalLastCmd === cmd) { console.log("[terminal] re-attaching to existing session for", cmd); // Force a resize in case geometry changed while the piece was gone, // and clear the local grid cache so the next paint pulls fresh state. if (cols > 0 && rows > 0) system.pty.resize(cols, rows); grid = null; started = true; return; }
globalThis.__terminalLastCmd = cmd; system.pty.spawn(cmd, args, cols, rows); started = true;}
function paint({ wipe, ink, box, write, qr, system, screen }) { const T = __theme.update(); const w = screen.width, h = screen.height; wipe(T.bgDim[0], T.bgDim[1], T.bgDim[2]); cursorBlink++;
const pty = system.pty; if (!pty.active) { if (pty.exitCode !== undefined) lastExitCode = pty.exitCode; ink(T.fgDim, T.fgDim, T.fgDim); write("terminal exited", { x: 10, y: 10, font: 1 });
if (lastExitCode === 127) { ink(T.err[0], T.err[1], T.err[2]); write(`'${lastCmd}' not found (exit 127)`, { x: 10, y: 24, font: 1 }); ink(T.warn[0], T.warn[1], T.warn[2]); write("command missing from initramfs", { x: 10, y: 38, font: 1 }); } else if (lastExitCode === 126) { ink(T.err[0], T.err[1], T.err[2]); write(`'${lastCmd}' permission denied (exit 126)`, { x: 10, y: 24, font: 1 }); } else if (lastExitCode > 128) { ink(T.err[0], T.err[1], T.err[2]); write(`killed by signal ${lastExitCode - 128}`, { x: 10, y: 24, font: 1 }); } else if (lastExitCode >= 0) { ink(lastExitCode === 0 ? T.ok[0] : T.err[0], lastExitCode === 0 ? T.ok[1] : T.warn[1], lastExitCode === 0 ? T.ok[2] : T.warn[2]); write(`exit code: ${lastExitCode}`, { x: 10, y: 24, font: 1 }); }
// Show last grid content if (grid) { const ptyCols = cols; let textY = 56; ink(T.fgMute, T.fgMute, T.fgMute); write("last output:", { x: 10, y: textY, font: 1 }); textY += 14; for (let y = 0; y < Math.min(rows, 10); y++) { let line = ""; for (let x = 0; x < ptyCols; x++) { const ch = grid[(y * ptyCols + x) * CELL_SIZE]; const rep = charReplace(ch); if (rep) line += rep; else if (line.length > 0) line += " "; } line = line.trimEnd(); if (line.length > 0) { ink(T.warn[0], T.warn[1], T.warn[2]); write(line, { x: 10, y: textY, font: 1 }); textY += 12; } } }
ink(T.fgMute, T.fgMute, T.fgMute); write("enter: retry esc: back", { x: 10, y: screen.height - 16, font: 1 }); return; }
// Cache grid data when available if (pty.grid) grid = pty.grid; if (!grid) return;
const ptyCols = pty.cols || cols; const ptyRows = pty.rows || rows;
// Check for terminal resize (screen changed since last frame) const newCols = Math.floor(w / cellW); const newRows = Math.floor(h / cellH); if (newCols !== cols || newRows !== rows) { cols = newCols; rows = newRows; if (cols > 0 && rows > 0) system.pty.resize(cols, rows); }
// Render each cell for (let y = 0; y < ptyRows; y++) { for (let x = 0; x < ptyCols; x++) { const i = (y * ptyCols + x) * CELL_SIZE; const ch = grid[i]; const fg = grid[i + 1]; const bg = grid[i + 2]; const bold = grid[i + 3]; const fg_r = grid[i + 4], fg_g = grid[i + 5], fg_b = grid[i + 6]; const bg_r = grid[i + 7], bg_g = grid[i + 8], bg_b = grid[i + 9];
const px = x * cellW; const py = y * cellH;
// Draw background if not default black const [br, bg2, bb] = bgColor(bg, bg_r, bg_g, bg_b); if (br !== 0 || bg2 !== 0 || bb !== 0) { ink(br, bg2, bb); box(px, py, cellW, cellH); }
// Draw character (ASCII + Unicode replacements) if (ch > 32) { const rep = charReplace(ch); if (rep) { const [fr, fg2, fb] = fgColor(fg, bold, fg_r, fg_g, fg_b); ink(fr, fg2, fb); write(rep, { x: px, y: py, font: 1 }); } } } }
// Blinking cursor — only when PTY reports cursor visible (?25h) if (pty.cursorVisible !== false && Math.floor(cursorBlink / 30) % 2 === 0) { const cx = (pty.cursorX || 0) * cellW; const cy = (pty.cursorY || 0) * cellH; // Solid block cursor — always visible regardless of cell color beneath ink(T.fg, T.fg, T.fg); box(cx, cy, cellW, cellH); // Redraw character at cursor position in background color (inverted) so it's readable const ci = ((pty.cursorY || 0) * ptyCols + (pty.cursorX || 0)) * CELL_SIZE; if (ci >= 0 && ci < grid.length) { const ch = grid[ci]; if (ch > 32) { const rep = charReplace(ch); if (rep) { ink(0, 0, 0); write(rep, { x: cx, y: cy, font: 1 }); } } } }}
// Keyboard → PTY input mappingconst SHIFT_MAP = { "1":"!", "2":"@", "3":"#", "4":"$", "5":"%", "6":"^", "7":"&", "8":"*", "9":"(", "0":")", "-":"_", "=":"+", "[":"{", "]":"}", "\\":"|", ";":":", "'":'"', ",":"<", ".":">", "/":"?", "`":"~",};
function keyToAnsi(key) { if (key === "arrowup") return "\x1b[A"; if (key === "arrowdown") return "\x1b[B"; if (key === "arrowright") return "\x1b[C"; if (key === "arrowleft") return "\x1b[D"; if (key === "home") return "\x1b[H"; if (key === "end") return "\x1b[F"; if (key === "pageup") return "\x1b[5~"; if (key === "pagedown") return "\x1b[6~"; if (key === "insert") return "\x1b[2~"; if (key === "delete") return "\x1b[3~"; if (key === "enter" || key === "return") return "\r"; if (key === "backspace") return "\x7f"; if (key === "tab") return "\t"; if (key === "escape") return "\x1b"; if (key === "f1") return "\x1bOP"; if (key === "f2") return "\x1bOQ"; if (key === "f3") return "\x1bOR"; if (key === "f4") return "\x1bOS"; if (key === "f5") return "\x1b[15~"; if (key === "f6") return "\x1b[17~"; if (key === "f7") return "\x1b[18~"; if (key === "f8") return "\x1b[19~"; if (key === "f9") return "\x1b[20~"; if (key === "f10") return "\x1b[21~"; if (key === "f11") return "\x1b[23~"; if (key === "f12") return "\x1b[24~"; return null;}
function act({ event: e, system }) { if (e.is("keyboard:down:shift")) { shiftHeld = true; return; } if (e.is("keyboard:up:shift")) { shiftHeld = false; return; } if (e.is("keyboard:down:control")) { ctrlHeld = true; return; } if (e.is("keyboard:up:control")) { ctrlHeld = false; return; } if (e.is("keyboard:down:alt")) { altHeld = true; return; } if (e.is("keyboard:up:alt")) { altHeld = false; return; }
if (!e.is("keyboard:down")) return; const key = e.key; if (!key) return;
// If terminal exited: escape goes back, enter retries if (!system.pty.active && started) { if (key === "escape" || key === "backspace") { system?.jump?.("prompt"); return; } if (key === "enter" || key === "return") { system.pty.spawn(lastCmd || "/bin/sh", [], cols, rows); grid = null; return; } return; }
// Plain Escape while PTY is running → jump back to prompt WITHOUT // killing the child. Claude sessions stay warm in the background; // re-entering `code` re-attaches to the same conversation. This // reclaims the workflow ergonomics of "esc to prompt, then back to // code" without having to re-bootstrap Claude every time. // // Users who need to send an actual ESC to the PTY (e.g. to cancel // a prompt in claude without leaving the piece) can use Ctrl+[ // which produces the same \x1b byte via the Ctrl+key handler below. if (key === "escape" && !ctrlHeld && !shiftHeld && !altHeld) { system?.jump?.("prompt"); return; }
// Ctrl+N — open split view (left=current cmd, right=sh) if (ctrlHeld && key === "n") { const name = lastCmd.split("/").pop() || "claude"; system.jump("split:" + name); return; }
// Ctrl+key → control character if (ctrlHeld && key.length === 1) { const code = key.toLowerCase().charCodeAt(0); if (code >= 97 && code <= 122) { system.pty.write(String.fromCharCode(code - 96)); return; } }
// Special keys → ANSI sequences const ansi = keyToAnsi(key); if (ansi) { system.pty.write(ansi); cursorBlink = 0; return; }
// Space bar if (key === "space") { system.pty.write(" "); cursorBlink = 0; return; }
// Printable characters if (key.length === 1) { let ch = key; if (shiftHeld) { if (SHIFT_MAP[ch]) ch = SHIFT_MAP[ch]; else ch = ch.toUpperCase(); } system.pty.write(ch); cursorBlink = 0; }}
function leave({ system }) { // Intentionally DO NOT kill the PTY child on piece-leave. Claude Code // sessions are expensive to re-establish (model handshake, project // scan, OAuth refresh) and losing one every time you bounce to the // prompt for a quick jump elsewhere is a workflow killer. Keeping the // child alive means re-entering `code` drops back into the same // session exactly where you left it. // // The PTY child still dies naturally when: // - it exits on its own (claude /exit, sh exit, etc.) // - the user explicitly kills from inside the session (Ctrl+C, etc.) // - ac-native itself exits (reboot, crash-respawn) // And `system.pty.kill()` is still available via Ctrl+\ shortcut for // users who really do want to terminate it. // No-op on leave. (void 0);}
export { boot, paint, act, leave };