apps/glyph Public The code is public; the app answers only to 1 person who can change it and the crew.

Files acd87b6b6146da11e0c9272c92a02a8e707adf4a
115 lines · 3.3 KB · ~970 tok Raw History
1
/**
2
 * The app in a terminal. The forge runs this on a pty when someone
3
 * `ssh`es to the app, over the same data as the page and through the same
4
 * actions, as the account at the keyboard. j/k move, a adds, d removes the
5
 * note under the cursor, q leaves. Plain ANSI, no dependencies.
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);
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts