pike/glyph Public Public: anyone can read this; nobody can change it.

forked from apps/glyph

workbenchthis machine
Files e45e4989a79ef3f801c453528dd5b471f91bea4f
114 lines · 3.7 KB · ~1.1k tok Raw History
1
/**
2
 * The app in a terminal. The forge runs this on a pty when someone `ssh`es
3
 * to the app, over the same data as the page and through the same actions,
4
 * as the account at the keyboard. A terminal cannot show a drawing, so it
5
 * keeps the list: j/k move, n asks for a new one, d removes the one under
6
 * the cursor, r reloads, q leaves. The page shows what was drawn.
7
 */
8
9
import {
10
  deleteDrawing,
11
  listDrawings,
12
  startDrawing,
13
  svgsOf,
14
  type Drawing,
15
} from "./actions.ts";
16
import { openDb } from "./db.ts";
17
import { drawerFromEnv, setDrawer } from "./draw.ts";
18
import { dataDir, publicOrigin, ssh, title, where } from "./env.ts";
19
import { nameOf } from "./glyph.ts";
20
import { whoAtTty } from "./who.ts";
21
22
const db = openDb();
23
const drawer = drawerFromEnv(dataDir);
24
if (drawer !== undefined) setDrawer(drawer);
25
const me = whoAtTty();
26
const esc = "\x1b[";
27
const dim = (s: string): string => `${esc}2m${s}${esc}0m`;
28
const bold = (s: string): string => `${esc}1m${s}${esc}0m`;
29
const out = (s: string): void => {
30
  process.stdout.write(s);
31
};
32
33
let cursor = 0;
34
let typing: string | undefined;
35
let said = "";
36
37
const drawings = (): Drawing[] => {
38
  const listed = listDrawings(db, me);
39
  return listed.ok ? listed.value : [];
40
};
41
42
const state = (d: Drawing): string =>
43
  d.status === "done"
44
    ? svgsOf(d).map(nameOf).join(" · ")
45
    : d.status === "drawing"
46
      ? "drawing…"
47
      : `failed: ${d.error ?? ""}`;
48
49
function draw(): void {
50
  const rows = drawings();
51
  const lines = [
52
    `${bold(title)}  ${dim(where)}  ${dim(me.role === "anon" ? "guest" : `${me.name}${me.role === "admin" ? " (admin)" : ""}`)}`,
53
    ssh === undefined ? "" : dim(`connect: ${ssh}`),
54
    dim(`see them at ${publicOrigin}`),
55
    "",
56
  ];
57
  if (rows.length === 0)
58
    lines.push(dim(me.role === "anon" ? "Sign in to draw." : "Nothing drawn yet. Press n."));
59
  rows.forEach((d, i) => {
60
    const prompt = i === cursor ? bold(d.prompt) : d.prompt;
61
    lines.push(`${i === cursor ? "▸" : " "} ${prompt}  ${dim(`${d.kind} · ${state(d)}`)}`);
62
  });
63
  lines.push("");
64
  if (typing !== undefined) lines.push(`Draw: ${typing}${esc}7m ${esc}0m`);
65
  else if (said !== "") lines.push(said);
66
  else lines.push(dim("j/k move · n new · d delete · r reload · q quit"));
67
  out(`${esc}2J${esc}H${lines.join("\r\n")}`);
68
}
69
70
process.stdin.setRawMode?.(true);
71
process.stdin.resume();
72
process.stdin.setEncoding("utf8");
73
draw();
74
// Claude answers in this process too: redraw now and then while one is out.
75
setInterval(() => {
76
  if (typing === undefined && drawings().some((d) => d.status === "drawing")) draw();
77
}, 3000);
78
process.stdin.on("data", (key: string) => {
79
  said = "";
80
  if (typing !== undefined) {
81
    if (key === "\r") {
82
      const started = startDrawing(db, me, { prompt: typing });
83
      said = started.ok ? "Drawing… it shows here and on the page when Claude is done." : started.message;
84
      typing = undefined;
85
      cursor = 0;
86
    } else if (key === "\x1b") typing = undefined;
87
    else if (key === "\x7f") typing = typing.slice(0, -1);
88
    else if (key >= " ") typing += key;
89
    draw();
90
    return;
91
  }
92
  if (key === "q" || key === "\x03") {
93
    out(`${esc}2J${esc}H`);
94
    process.exit(0);
95
  }
96
  const rows = drawings();
97
  if (key === "j" || key === "\x1b[B") cursor = Math.min(cursor + 1, Math.max(0, rows.length - 1));
98
  if (key === "k" || key === "\x1b[A") cursor = Math.max(cursor - 1, 0);
99
  if (key === "n") {
100
    if (me.role === "anon") said = "Sign in to draw.";
101
    else typing = "";
102
  }
103
  if (key === "d") {
104
    const d = rows[cursor];
105
    if (d === undefined) said = "Nothing to remove.";
106
    else {
107
      const removed = deleteDrawing(db, me, { id: d.id });
108
      if (!removed.ok) said = removed.message;
109
      cursor = Math.min(cursor, Math.max(0, rows.length - 2));
110
    }
111
  }
112
  draw();
113
});
114
process.stdout.on("resize", draw);
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts