/** * 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. j/k move, a adds, d removes the * note under the cursor, q leaves. Plain ANSI, no dependencies. */ import { addNote, canDelete, deleteNote, listNotes, type Note, } from "./actions.ts"; import { openDb } from "./db.ts"; import { ssh, title, where } from "./env.ts"; import { whoAtTty } from "./who.ts"; const db = openDb(); 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 notes = (): Note[] => { const listed = listNotes(db, me); return listed.ok ? listed.value : []; }; function draw(): void { const rows = notes(); const lines = [ `${bold(title)} ${dim(where)} ${dim(me.role === "anon" ? "guest" : `${me.name}${me.role === "admin" ? " (admin)" : ""}`)}`, ssh === undefined ? "" : dim(`connect: ${ssh}`), "", ]; if (rows.length === 0) lines.push( dim( me.role === "anon" ? "No notes yet." : "No notes yet. Press a to add one.", ), ); rows.forEach((note, i) => { const body = i === cursor ? bold(note.body) : note.body; lines.push( `${i === cursor ? "▸" : " "} ${body} ${dim(`${note.author_name} · ${note.created_at.slice(0, 16).replace("T", " ")}`)}`, ); }); lines.push(""); if (typing !== undefined) lines.push(`New note: ${typing}${esc}7m ${esc}0m`); else if (said !== "") lines.push(said); else lines.push( dim( me.role === "anon" ? "j/k move · q quit · sign in with a key the forge knows to add" : "j/k move · a add · d delete · q quit", ), ); out(`${esc}2J${esc}H${lines.join("\r\n")}`); } process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdin.setEncoding("utf8"); draw(); process.stdin.on("data", (key: string) => { said = ""; if (typing !== undefined) { if (key === "\r") { const added = addNote(db, me, { body: typing }); if (!added.ok) said = added.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 = notes(); 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 === "a") { if (me.role === "anon") said = "Sign in to add a note."; else typing = ""; } if (key === "d") { const note = rows[cursor]; if (note === undefined) said = "Nothing to remove."; else if (!canDelete(me, note)) said = "Only the author or an admin can remove a note."; else { const removed = deleteNote(db, me, { id: note.id }); if (!removed.ok) said = removed.message; cursor = Math.min(cursor, Math.max(0, rows.length - 2)); } } draw(); }); process.stdout.on("resize", draw);