wren/glyph Public Public: anyone can read this; nobody can change it.
forked from apps/glyph
workbenchthis machine
Files 5c2d82ded0cf18c81c70d9f4a3f1de0b1688b125
| 1 | /** |
| 2 | * The app in a terminal. The forge runs this on a pty when someone `ssh`es |
| 3 | * to the app, over the same data as the page and through the same actions, |
| 4 | * as the account at the keyboard. A terminal cannot show a drawing, so it |
| 5 | * keeps the list: j/k move, n asks for a new one, d removes the one under |
| 6 | * the cursor, r reloads, q leaves. The page shows what was drawn. |
| 7 | */ |
| 8 | |
| 9 | import { |
| 10 | deleteDrawing, |
| 11 | listDrawings, |
| 12 | startDrawing, |
| 13 | svgsOf, |
| 14 | type Drawing, |
| 15 | } from "./actions.ts"; |
| 16 | import { openDb } from "./db.ts"; |
| 17 | import { drawerFromEnv, setDrawer } from "./draw.ts"; |
| 18 | import { dataDir, publicOrigin, ssh, title, where } from "./env.ts"; |
| 19 | import { nameOf } from "./glyph.ts"; |
| 20 | import { whoAtTty } from "./who.ts"; |
| 21 | |
| 22 | const db = openDb(); |
| 23 | const drawer = drawerFromEnv(dataDir); |
| 24 | if (drawer !== undefined) setDrawer(drawer); |
| 25 | const me = whoAtTty(); |
| 26 | const esc = "\x1b["; |
| 27 | const dim = (s: string): string => `${esc}2m${s}${esc}0m`; |
| 28 | const bold = (s: string): string => `${esc}1m${s}${esc}0m`; |
| 29 | const out = (s: string): void => { |
| 30 | process.stdout.write(s); |
| 31 | }; |
| 32 | |
| 33 | let cursor = 0; |
| 34 | let typing: string | undefined; |
| 35 | let said = ""; |
| 36 | |
| 37 | const drawings = (): Drawing[] => { |
| 38 | const listed = listDrawings(db, me); |
| 39 | return listed.ok ? listed.value : []; |
| 40 | }; |
| 41 | |
| 42 | const state = (d: Drawing): string => |
| 43 | d.status === "done" |
| 44 | ? svgsOf(d).map(nameOf).join(" · ") |
| 45 | : d.status === "drawing" |
| 46 | ? "drawing…" |
| 47 | : `failed: ${d.error ?? ""}`; |
| 48 | |
| 49 | function draw(): void { |
| 50 | const rows = drawings(); |
| 51 | const lines = [ |
| 52 | `${bold(title)} ${dim(where)} ${dim(me.role === "anon" ? "guest" : `${me.name}${me.role === "admin" ? " (admin)" : ""}`)}`, |
| 53 | ssh === undefined ? "" : dim(`connect: ${ssh}`), |
| 54 | dim(`see them at ${publicOrigin}`), |
| 55 | "", |
| 56 | ]; |
| 57 | if (rows.length === 0) |
| 58 | lines.push(dim(me.role === "anon" ? "Sign in to draw." : "Nothing drawn yet. Press n.")); |
| 59 | rows.forEach((d, i) => { |
| 60 | const prompt = i === cursor ? bold(d.prompt) : d.prompt; |
| 61 | lines.push(`${i === cursor ? "▸" : " "} ${prompt} ${dim(`${d.kind} · ${state(d)}`)}`); |
| 62 | }); |
| 63 | lines.push(""); |
| 64 | if (typing !== undefined) lines.push(`Draw: ${typing}${esc}7m ${esc}0m`); |
| 65 | else if (said !== "") lines.push(said); |
| 66 | else lines.push(dim("j/k move · n new · d delete · r reload · q quit")); |
| 67 | out(`${esc}2J${esc}H${lines.join("\r\n")}`); |
| 68 | } |
| 69 | |
| 70 | process.stdin.setRawMode?.(true); |
| 71 | process.stdin.resume(); |
| 72 | process.stdin.setEncoding("utf8"); |
| 73 | draw(); |
| 74 | // Claude answers in this process too: redraw now and then while one is out. |
| 75 | setInterval(() => { |
| 76 | if (typing === undefined && drawings().some((d) => d.status === "drawing")) draw(); |
| 77 | }, 3000); |
| 78 | process.stdin.on("data", (key: string) => { |
| 79 | said = ""; |
| 80 | if (typing !== undefined) { |
| 81 | if (key === "\r") { |
| 82 | const started = startDrawing(db, me, { prompt: typing }); |
| 83 | said = started.ok ? "Drawing… it shows here and on the page when Claude is done." : started.message; |
| 84 | typing = undefined; |
| 85 | cursor = 0; |
| 86 | } else if (key === "\x1b") typing = undefined; |
| 87 | else if (key === "\x7f") typing = typing.slice(0, -1); |
| 88 | else if (key >= " ") typing += key; |
| 89 | draw(); |
| 90 | return; |
| 91 | } |
| 92 | if (key === "q" || key === "\x03") { |
| 93 | out(`${esc}2J${esc}H`); |
| 94 | process.exit(0); |
| 95 | } |
| 96 | const rows = drawings(); |
| 97 | if (key === "j" || key === "\x1b[B") cursor = Math.min(cursor + 1, Math.max(0, rows.length - 1)); |
| 98 | if (key === "k" || key === "\x1b[A") cursor = Math.max(cursor - 1, 0); |
| 99 | if (key === "n") { |
| 100 | if (me.role === "anon") said = "Sign in to draw."; |
| 101 | else typing = ""; |
| 102 | } |
| 103 | if (key === "d") { |
| 104 | const d = rows[cursor]; |
| 105 | if (d === undefined) said = "Nothing to remove."; |
| 106 | else { |
| 107 | const removed = deleteDrawing(db, me, { id: d.id }); |
| 108 | if (!removed.ok) said = removed.message; |
| 109 | cursor = Math.min(cursor, Math.max(0, rows.length - 2)); |
| 110 | } |
| 111 | } |
| 112 | draw(); |
| 113 | }); |
| 114 | process.stdout.on("resize", draw); |