| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
import { |
| 9 |
addNote, |
| 10 |
canDelete, |
| 11 |
deleteNote, |
| 12 |
listNotes, |
| 13 |
type Note, |
| 14 |
} from "./actions.ts"; |
| 15 |
import { openDb } from "./db.ts"; |
| 16 |
import { ssh, title, where } from "./env.ts"; |
| 17 |
import { whoAtTty } from "./who.ts"; |
| 18 |
|
| 19 |
const db = openDb(); |
| 20 |
const me = whoAtTty(); |
| 21 |
const esc = "\x1b["; |
| 22 |
const dim = (s: string): string => `${esc}2m${s}${esc}0m`; |
| 23 |
const bold = (s: string): string => `${esc}1m${s}${esc}0m`; |
| 24 |
const out = (s: string): void => { |
| 25 |
process.stdout.write(s); |
| 26 |
}; |
| 27 |
|
| 28 |
let cursor = 0; |
| 29 |
let typing: string | undefined; |
| 30 |
let said = ""; |
| 31 |
|
| 32 |
const notes = (): Note[] => { |
| 33 |
const listed = listNotes(db, me); |
| 34 |
return listed.ok ? listed.value : []; |
| 35 |
}; |
| 36 |
|
| 37 |
function draw(): void { |
| 38 |
const rows = notes(); |
| 39 |
const lines = [ |
| 40 |
`${bold(title)} ${dim(where)} ${dim(me.role === "anon" ? "guest" : `${me.name}${me.role === "admin" ? " (admin)" : ""}`)}`, |
| 41 |
ssh === undefined ? "" : dim(`connect: ${ssh}`), |
| 42 |
"", |
| 43 |
]; |
| 44 |
if (rows.length === 0) |
| 45 |
lines.push( |
| 46 |
dim( |
| 47 |
me.role === "anon" |
| 48 |
? "No notes yet." |
| 49 |
: "No notes yet. Press a to add one.", |
| 50 |
), |
| 51 |
); |
| 52 |
rows.forEach((note, i) => { |
| 53 |
const body = i === cursor ? bold(note.body) : note.body; |
| 54 |
lines.push( |
| 55 |
`${i === cursor ? "▸" : " "} ${body} ${dim(`${note.author_name} · ${note.created_at.slice(0, 16).replace("T", " ")}`)}`, |
| 56 |
); |
| 57 |
}); |
| 58 |
lines.push(""); |
| 59 |
if (typing !== undefined) lines.push(`New note: ${typing}${esc}7m ${esc}0m`); |
| 60 |
else if (said !== "") lines.push(said); |
| 61 |
else |
| 62 |
lines.push( |
| 63 |
dim( |
| 64 |
me.role === "anon" |
| 65 |
? "j/k move · q quit · sign in with a key the forge knows to add" |
| 66 |
: "j/k move · a add · d delete · q quit", |
| 67 |
), |
| 68 |
); |
| 69 |
out(`${esc}2J${esc}H${lines.join("\r\n")}`); |
| 70 |
} |
| 71 |
|
| 72 |
process.stdin.setRawMode?.(true); |
| 73 |
process.stdin.resume(); |
| 74 |
process.stdin.setEncoding("utf8"); |
| 75 |
draw(); |
| 76 |
process.stdin.on("data", (key: string) => { |
| 77 |
said = ""; |
| 78 |
if (typing !== undefined) { |
| 79 |
if (key === "\r") { |
| 80 |
const added = addNote(db, me, { body: typing }); |
| 81 |
if (!added.ok) said = added.message; |
| 82 |
typing = undefined; |
| 83 |
cursor = 0; |
| 84 |
} else if (key === "\x1b") typing = undefined; |
| 85 |
else if (key === "\x7f") typing = typing.slice(0, -1); |
| 86 |
else if (key >= " ") typing += key; |
| 87 |
draw(); |
| 88 |
return; |
| 89 |
} |
| 90 |
if (key === "q" || key === "\x03") { |
| 91 |
out(`${esc}2J${esc}H`); |
| 92 |
process.exit(0); |
| 93 |
} |
| 94 |
const rows = notes(); |
| 95 |
if (key === "j" || key === "\x1b[B") |
| 96 |
cursor = Math.min(cursor + 1, Math.max(0, rows.length - 1)); |
| 97 |
if (key === "k" || key === "\x1b[A") cursor = Math.max(cursor - 1, 0); |
| 98 |
if (key === "a") { |
| 99 |
if (me.role === "anon") said = "Sign in to add a note."; |
| 100 |
else typing = ""; |
| 101 |
} |
| 102 |
if (key === "d") { |
| 103 |
const note = rows[cursor]; |
| 104 |
if (note === undefined) said = "Nothing to remove."; |
| 105 |
else if (!canDelete(me, note)) |
| 106 |
said = "Only the author or an admin can remove a note."; |
| 107 |
else { |
| 108 |
const removed = deleteNote(db, me, { id: note.id }); |
| 109 |
if (!removed.ok) said = removed.message; |
| 110 |
cursor = Math.min(cursor, Math.max(0, rows.length - 2)); |
| 111 |
} |
| 112 |
} |
| 113 |
draw(); |
| 114 |
}); |
| 115 |
process.stdout.on("resize", draw); |