/** * The app in a terminal. The forge runs this on a pty when someone `ssh`es * to the app, over the same data as the page and through the same actions, * as the account at the keyboard. A terminal cannot show a drawing, so it * keeps the list: j/k move, n asks for a new one, d removes the one under * the cursor, r reloads, q leaves. The page shows what was drawn. */ import { deleteDrawing, listDrawings, startDrawing, svgsOf, type Drawing, } from "./actions.ts"; import { openDb } from "./db.ts"; import { drawerFromEnv, setDrawer } from "./draw.ts"; import { dataDir, publicOrigin, ssh, title, where } from "./env.ts"; import { nameOf } from "./glyph.ts"; import { whoAtTty } from "./who.ts"; const db = openDb(); const drawer = drawerFromEnv(dataDir); if (drawer !== undefined) setDrawer(drawer); const me = whoAtTty(); const esc = "\x1b["; const dim = (s: string): string => `${esc}2m${s}${esc}0m`; const bold = (s: string): string => `${esc}1m${s}${esc}0m`; const out = (s: string): void => { process.stdout.write(s); }; let cursor = 0; let typing: string | undefined; let said = ""; const drawings = (): Drawing[] => { const listed = listDrawings(db, me); return listed.ok ? listed.value : []; }; const state = (d: Drawing): string => d.status === "done" ? svgsOf(d).map(nameOf).join(" · ") : d.status === "drawing" ? "drawing…" : `failed: ${d.error ?? ""}`; function draw(): void { const rows = drawings(); const lines = [ `${bold(title)} ${dim(where)} ${dim(me.role === "anon" ? "guest" : `${me.name}${me.role === "admin" ? " (admin)" : ""}`)}`, ssh === undefined ? "" : dim(`connect: ${ssh}`), dim(`see them at ${publicOrigin}`), "", ]; if (rows.length === 0) lines.push(dim(me.role === "anon" ? "Sign in to draw." : "Nothing drawn yet. Press n.")); rows.forEach((d, i) => { const prompt = i === cursor ? bold(d.prompt) : d.prompt; lines.push(`${i === cursor ? "▸" : " "} ${prompt} ${dim(`${d.kind} · ${state(d)}`)}`); }); lines.push(""); if (typing !== undefined) lines.push(`Draw: ${typing}${esc}7m ${esc}0m`); else if (said !== "") lines.push(said); else lines.push(dim("j/k move · n new · d delete · r reload · q quit")); out(`${esc}2J${esc}H${lines.join("\r\n")}`); } process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdin.setEncoding("utf8"); draw(); // Claude answers in this process too: redraw now and then while one is out. setInterval(() => { if (typing === undefined && drawings().some((d) => d.status === "drawing")) draw(); }, 3000); process.stdin.on("data", (key: string) => { said = ""; if (typing !== undefined) { if (key === "\r") { const started = startDrawing(db, me, { prompt: typing }); said = started.ok ? "Drawing… it shows here and on the page when Claude is done." : started.message; typing = undefined; cursor = 0; } else if (key === "\x1b") typing = undefined; else if (key === "\x7f") typing = typing.slice(0, -1); else if (key >= " ") typing += key; draw(); return; } if (key === "q" || key === "\x03") { out(`${esc}2J${esc}H`); process.exit(0); } const rows = drawings(); if (key === "j" || key === "\x1b[B") cursor = Math.min(cursor + 1, Math.max(0, rows.length - 1)); if (key === "k" || key === "\x1b[A") cursor = Math.max(cursor - 1, 0); if (key === "n") { if (me.role === "anon") said = "Sign in to draw."; else typing = ""; } if (key === "d") { const d = rows[cursor]; if (d === undefined) said = "Nothing to remove."; else { const removed = deleteDrawing(db, me, { id: d.id }); if (!removed.ok) said = removed.message; cursor = Math.min(cursor, Math.max(0, rows.length - 2)); } } draw(); }); process.stdout.on("resize", draw);