/** * The oracle in a terminal. The forge runs this on a pty when someone * `ssh`es to the app, with FORGE_APP_DATA pointing at the same ledger the * web page reads: j/k move, enter opens nothing because everything is * already on screen, `a` asks, `r`/`w` score the verdict under the cursor, * `u` takes a score back, `q` leaves. Plain ANSI, no dependencies. */ import * as ledger from "./db.ts"; import { calibrate, verdictOnItself } from "./oracle.ts"; const db = ledger.open(); const where = process.env.FORGE_APP_ENV === "preview" ? `preview of #${process.env.FORGE_APP_PULL ?? "?"}` : (process.env.FORGE_APP_ENV ?? "local"); const esc = "\x1b["; const out = (s: string): void => { process.stdout.write(s); }; const dim = (s: string): string => `${esc}2m${s}${esc}0m`; const bold = (s: string): string => `${esc}1m${s}${esc}0m`; const green = (s: string): string => `${esc}32m${s}${esc}0m`; const red = (s: string): string => `${esc}31m${s}${esc}0m`; let cursor = 0; let typing: string | undefined; let flash = ""; function mark(row: ledger.Row): string { if (row.outcome === "right") return green("right"); if (row.outcome === "wrong") return red("wrong"); return dim("unjudged"); } function draw(): void { const rows = ledger.ledger(db, "all", 100); if (cursor >= rows.length) cursor = Math.max(0, rows.length - 1); const width = Math.max(40, (process.stdout.columns ?? 80) - 2); const clip = (s: string, n: number): string => s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}…`; out(`${esc}2J${esc}H`); out(`${bold("the oracle")} ${dim(`· ${where} · ${rows.length} verdict${rows.length === 1 ? "" : "s"}`)}\r\n`); out(`${dim(verdictOnItself(calibrate(ledger.scored(db))))}\r\n\r\n`); if (rows.length === 0) out(dim(" nothing asked yet — press a\r\n")); for (const [i, row] of rows.entries()) { const here = i === cursor; out(`${here ? bold("▸ ") : " "}${clip(row.question, width - 22)}\r\n`); out(` ${dim(`${row.confidence}% · ${mark(row)}`)}\r\n`); if (here) out(` ${clip(row.stance, width - 6)}\r\n ${dim(clip(row.auspice, width - 6))}\r\n`); } out(`\r\n${flash === "" ? dim("j/k move · a ask · r right · w wrong · u unjudge · q quit") : flash}\r\n`); if (typing !== undefined) out(`\r\n${bold("ask:")} ${typing}${dim("▏")}\r\n`); } function judgeUnderCursor(outcome: "right" | "wrong" | "unjudged"): void { const rows = ledger.ledger(db, "all", 100); const row = rows[cursor]; if (row === undefined) return; ledger.judge(db, row.id, outcome); flash = dim(`scored #${row.id} ${outcome}`); } function key(data: Buffer): void { const s = data.toString("utf8"); if (typing !== undefined) { if (s === "\r" || s === "\n") { const question = typing.trim(); typing = undefined; if (question !== "") { const row = ledger.ask(db, question); flash = dim(`the oracle: ${row.stance}`); cursor = 0; } } else if (s === "\x7f") typing = typing.slice(0, -1); else if (s === "\x1b" || s === "\x03") typing = undefined; else if (s >= " " && typing.length < 280) typing += s; draw(); return; } flash = ""; if (s === "q" || s === "\x03" || s === "\x04") { out(`${esc}2J${esc}H`); db.close(); process.exit(0); } else if (s === "j" || s === `${esc}B`) cursor += 1; else if (s === "k" || s === `${esc}A`) cursor = Math.max(0, cursor - 1); else if (s === "a") typing = ""; else if (s === "r") judgeUnderCursor("right"); else if (s === "w") judgeUnderCursor("wrong"); else if (s === "u") judgeUnderCursor("unjudged"); draw(); } if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.on("data", key); process.stdout.on("resize", draw); draw();