quill/oracle Public Public: anyone can read this; nobody can change it.
workbenchthis machine
Files f5bda0fa490dc51bf72b54ac9776717cf7b1ae79
| 1 | /** |
| 2 | * The oracle in a terminal. The forge runs this on a pty when someone |
| 3 | * `ssh`es to the app, with FORGE_APP_DATA pointing at the same ledger the |
| 4 | * web page reads: j/k move, enter opens nothing because everything is |
| 5 | * already on screen, `a` asks, `r`/`w` score the verdict under the cursor, |
| 6 | * `u` takes a score back, `q` leaves. Plain ANSI, no dependencies. |
| 7 | */ |
| 8 | |
| 9 | import * as ledger from "./db.ts"; |
| 10 | import { calibrate, verdictOnItself } from "./oracle.ts"; |
| 11 | |
| 12 | const db = ledger.open(); |
| 13 | const where = |
| 14 | process.env.FORGE_APP_ENV === "preview" |
| 15 | ? `preview of #${process.env.FORGE_APP_PULL ?? "?"}` |
| 16 | : (process.env.FORGE_APP_ENV ?? "local"); |
| 17 | |
| 18 | const esc = "\x1b["; |
| 19 | const out = (s: string): void => { |
| 20 | process.stdout.write(s); |
| 21 | }; |
| 22 | const dim = (s: string): string => `${esc}2m${s}${esc}0m`; |
| 23 | const bold = (s: string): string => `${esc}1m${s}${esc}0m`; |
| 24 | const green = (s: string): string => `${esc}32m${s}${esc}0m`; |
| 25 | const red = (s: string): string => `${esc}31m${s}${esc}0m`; |
| 26 | |
| 27 | let cursor = 0; |
| 28 | let typing: string | undefined; |
| 29 | let flash = ""; |
| 30 | |
| 31 | function mark(row: ledger.Row): string { |
| 32 | if (row.outcome === "right") return green("right"); |
| 33 | if (row.outcome === "wrong") return red("wrong"); |
| 34 | return dim("unjudged"); |
| 35 | } |
| 36 | |
| 37 | function draw(): void { |
| 38 | const rows = ledger.ledger(db, "all", 100); |
| 39 | if (cursor >= rows.length) cursor = Math.max(0, rows.length - 1); |
| 40 | const width = Math.max(40, (process.stdout.columns ?? 80) - 2); |
| 41 | const clip = (s: string, n: number): string => |
| 42 | s.length <= n ? s : `${s.slice(0, Math.max(0, n - 1))}…`; |
| 43 | out(`${esc}2J${esc}H`); |
| 44 | out(`${bold("the oracle")} ${dim(`· ${where} · ${rows.length} verdict${rows.length === 1 ? "" : "s"}`)}\r\n`); |
| 45 | out(`${dim(verdictOnItself(calibrate(ledger.scored(db))))}\r\n\r\n`); |
| 46 | if (rows.length === 0) out(dim(" nothing asked yet — press a\r\n")); |
| 47 | for (const [i, row] of rows.entries()) { |
| 48 | const here = i === cursor; |
| 49 | out(`${here ? bold("▸ ") : " "}${clip(row.question, width - 22)}\r\n`); |
| 50 | out(` ${dim(`${row.confidence}% · ${mark(row)}`)}\r\n`); |
| 51 | if (here) out(` ${clip(row.stance, width - 6)}\r\n ${dim(clip(row.auspice, width - 6))}\r\n`); |
| 52 | } |
| 53 | out(`\r\n${flash === "" ? dim("j/k move · a ask · r right · w wrong · u unjudge · q quit") : flash}\r\n`); |
| 54 | if (typing !== undefined) out(`\r\n${bold("ask:")} ${typing}${dim("▏")}\r\n`); |
| 55 | } |
| 56 | |
| 57 | function judgeUnderCursor(outcome: "right" | "wrong" | "unjudged"): void { |
| 58 | const rows = ledger.ledger(db, "all", 100); |
| 59 | const row = rows[cursor]; |
| 60 | if (row === undefined) return; |
| 61 | ledger.judge(db, row.id, outcome); |
| 62 | flash = dim(`scored #${row.id} ${outcome}`); |
| 63 | } |
| 64 | |
| 65 | function key(data: Buffer): void { |
| 66 | const s = data.toString("utf8"); |
| 67 | if (typing !== undefined) { |
| 68 | if (s === "\r" || s === "\n") { |
| 69 | const question = typing.trim(); |
| 70 | typing = undefined; |
| 71 | if (question !== "") { |
| 72 | const row = ledger.ask(db, question); |
| 73 | flash = dim(`the oracle: ${row.stance}`); |
| 74 | cursor = 0; |
| 75 | } |
| 76 | } else if (s === "\x7f") typing = typing.slice(0, -1); |
| 77 | else if (s === "\x1b" || s === "\x03") typing = undefined; |
| 78 | else if (s >= " " && typing.length < 280) typing += s; |
| 79 | draw(); |
| 80 | return; |
| 81 | } |
| 82 | flash = ""; |
| 83 | if (s === "q" || s === "\x03" || s === "\x04") { |
| 84 | out(`${esc}2J${esc}H`); |
| 85 | db.close(); |
| 86 | process.exit(0); |
| 87 | } else if (s === "j" || s === `${esc}B`) cursor += 1; |
| 88 | else if (s === "k" || s === `${esc}A`) cursor = Math.max(0, cursor - 1); |
| 89 | else if (s === "a") typing = ""; |
| 90 | else if (s === "r") judgeUnderCursor("right"); |
| 91 | else if (s === "w") judgeUnderCursor("wrong"); |
| 92 | else if (s === "u") judgeUnderCursor("unjudged"); |
| 93 | draw(); |
| 94 | } |
| 95 | |
| 96 | if (process.stdin.isTTY) process.stdin.setRawMode(true); |
| 97 | process.stdin.on("data", key); |
| 98 | process.stdout.on("resize", draw); |
| 99 | draw(); |