accelr8 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,593 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CANVAS_H,
4
+ CANVAS_W,
5
+ DEFAULT_SERVER,
6
+ decode,
7
+ encode
8
+ } from "./chunk-BVRGM7YI.js";
9
+
10
+ // src/client.ts
11
+ import tk from "terminal-kit";
12
+ import { WebSocket } from "ws";
13
+ import { spawn } from "node:child_process";
14
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ // src/logo.ts
19
+ var LOGO = [
20
+ " ('-. ('-. _ .-')",
21
+ " ( OO ).-. _( OO) ( \\( -O )",
22
+ " / . --. / .-----. .-----. (,------. ,--. ,------. .-----.",
23
+ " | \\-. \\ ' .--./ ' .--./ | .---' | |.-') | /`. ' / .-. \\",
24
+ ".-'-' | | | |('-. | |('-. | | | | OO ) | / | | | \\_.' /",
25
+ " \\| |_.' | /_) |OO ) /_) |OO ) (| '--. | |`-' | | |_.' | / .-. '.",
26
+ " | .-. | || |`-'| || |`-'| | .--' (| '---.' | . '.' | | | |",
27
+ " | | | | (_' '--'\\ (_' '--'\\ | `---. | | | |\\ \\ \\ '-' /",
28
+ " `--' `--' `-----' `-----' `------' `------' `--' '--' `----''"
29
+ ];
30
+ var LOGO_W = Math.max(...LOGO.map((l) => l.length));
31
+
32
+ // src/client.ts
33
+ var term = tk.terminal;
34
+ var ScreenBuffer = tk.ScreenBuffer;
35
+ var CONF_DIR = join(homedir(), ".accelr8");
36
+ var ID_FILE = join(CONF_DIR, "identity.json");
37
+ var USER_COLORS = [39, 213, 220, 46, 208, 141, 51, 203, 118, 165];
38
+ function loadIdentity(nameOverride) {
39
+ let saved = {};
40
+ if (existsSync(ID_FILE)) {
41
+ try {
42
+ saved = JSON.parse(readFileSync(ID_FILE, "utf8"));
43
+ } catch {
44
+ }
45
+ }
46
+ const id = saved.id || "u_" + Math.random().toString(36).slice(2, 10);
47
+ const name = (nameOverride || saved.name || process.env.USER || "guest").slice(0, 16);
48
+ let h = 0;
49
+ for (const ch of id) h = h * 31 + ch.charCodeAt(0) >>> 0;
50
+ const color = USER_COLORS[h % USER_COLORS.length];
51
+ try {
52
+ mkdirSync(CONF_DIR, { recursive: true });
53
+ writeFileSync(ID_FILE, JSON.stringify({ id, name }));
54
+ } catch {
55
+ }
56
+ return { id, name, color };
57
+ }
58
+ var me = { id: "u_local", name: process.env.USER || "guest", color: 39 };
59
+ var SERVER = DEFAULT_SERVER;
60
+ var autoJoinCode;
61
+ var state = {
62
+ screen: "projects",
63
+ connected: false,
64
+ projects: [],
65
+ project: null,
66
+ members: [],
67
+ online: /* @__PURE__ */ new Set(),
68
+ grid: {},
69
+ brush: "\u2588",
70
+ status: "",
71
+ modal: null
72
+ };
73
+ var cursors = /* @__PURE__ */ new Map();
74
+ var myLogical = null;
75
+ var painting = false;
76
+ var lastPaintKey = "";
77
+ var cursorDirty = false;
78
+ var PALETTE = ["\u2588", "\u2593", "\u2592", "\u2591", "\u25CF", "\u25C6", "\u2605", "\u25B2", "\u25A0", "\u2550", "\u2551", "\xB7", "#", "@", "*", "+"];
79
+ var IO_X = 1;
80
+ var IO_Y = 5;
81
+ var ws = null;
82
+ var reconnectTimer = null;
83
+ var rejoining = false;
84
+ var lastPong = 0;
85
+ function connect() {
86
+ state.status = "connecting to " + SERVER + " ...";
87
+ try {
88
+ ws = new WebSocket(SERVER);
89
+ } catch {
90
+ state.status = "invalid server url: " + SERVER;
91
+ return;
92
+ }
93
+ ws.on("open", () => {
94
+ state.connected = true;
95
+ state.status = "";
96
+ lastPong = Date.now();
97
+ sendMsg({ t: "hello", user: me });
98
+ if (autoJoinCode && !state.project) sendMsg({ t: "joinProject", code: autoJoinCode });
99
+ else if (state.project) {
100
+ rejoining = true;
101
+ sendMsg({ t: "joinProject", code: state.project.code });
102
+ }
103
+ });
104
+ ws.on("message", (raw) => {
105
+ lastPong = Date.now();
106
+ try {
107
+ handleServer(decode(raw.toString()));
108
+ } catch {
109
+ }
110
+ });
111
+ ws.on("pong", () => {
112
+ lastPong = Date.now();
113
+ });
114
+ ws.on("close", () => {
115
+ state.connected = false;
116
+ state.status = "disconnected, retrying...";
117
+ if (!reconnectTimer) reconnectTimer = setTimeout(() => {
118
+ reconnectTimer = null;
119
+ connect();
120
+ }, 2e3);
121
+ });
122
+ ws.on("error", () => {
123
+ });
124
+ }
125
+ function sendMsg(msg) {
126
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
127
+ }
128
+ function handleServer(msg) {
129
+ if (!msg || typeof msg !== "object" || typeof msg.t !== "string") return;
130
+ switch (msg.t) {
131
+ case "welcome":
132
+ break;
133
+ case "projects":
134
+ state.projects = Array.isArray(msg.list) ? msg.list : [];
135
+ break;
136
+ case "joined":
137
+ if (!msg.project || typeof msg.project.code !== "string" || typeof msg.project.name !== "string") break;
138
+ state.project = msg.project;
139
+ state.grid = msg.grid || {};
140
+ state.members = Array.isArray(msg.members) ? msg.members : [];
141
+ cursors.clear();
142
+ if (!rejoining) {
143
+ state.screen = "canvas";
144
+ state.status = `joined "${msg.project.name}"`;
145
+ }
146
+ rejoining = false;
147
+ break;
148
+ case "joinError":
149
+ state.status = msg.message;
150
+ break;
151
+ case "members":
152
+ state.members = msg.members;
153
+ break;
154
+ case "presence":
155
+ state.online = new Set(msg.online);
156
+ break;
157
+ case "cursor": {
158
+ let c = cursors.get(msg.id);
159
+ const m = state.members.find((x) => x.id === msg.id);
160
+ if (!c) {
161
+ c = { tx: msg.x, ty: msg.y, x: msg.x, y: msg.y, name: m?.name || "?", color: m?.color || 7, seen: Date.now() };
162
+ cursors.set(msg.id, c);
163
+ }
164
+ c.tx = msg.x;
165
+ c.ty = msg.y;
166
+ c.seen = Date.now();
167
+ if (m) {
168
+ c.name = m.name;
169
+ c.color = m.color;
170
+ }
171
+ break;
172
+ }
173
+ case "cursorGone":
174
+ cursors.delete(msg.id);
175
+ break;
176
+ case "paint":
177
+ if (msg.char === " ") delete state.grid[`${msg.x},${msg.y}`];
178
+ else state.grid[`${msg.x},${msg.y}`] = [msg.char, msg.color];
179
+ break;
180
+ case "cleared":
181
+ state.grid = {};
182
+ break;
183
+ }
184
+ }
185
+ var screen;
186
+ function makeScreen() {
187
+ const width = Math.max(40, term.width || process.stdout.columns || 120);
188
+ const height = Math.max(12, term.height || process.stdout.rows || 40);
189
+ screen = new ScreenBuffer({ dst: term, width, height });
190
+ }
191
+ function __setHeadlessScreen(width, height) {
192
+ screen = new ScreenBuffer({ width, height });
193
+ }
194
+ function __dump() {
195
+ return screen.dumpChars();
196
+ }
197
+ var H = () => screen.height;
198
+ var W = () => screen.width;
199
+ function puts(x, y, str, attr) {
200
+ if (y < 0 || y >= H()) return;
201
+ const clean = String(str).replace(/[\x00-\x1f\x7f]/g, " ");
202
+ screen.put({ x, y, attr, dx: 1, dy: 0, wrap: false }, clean);
203
+ }
204
+ var clickables = [];
205
+ function button(x, y, label, attr, action) {
206
+ puts(x, y, label, attr);
207
+ clickables.push({ x0: x, y0: y, x1: x + label.length - 1, y1: y, action });
208
+ return x + label.length;
209
+ }
210
+ var C = {
211
+ bg: 233,
212
+ panel: 234,
213
+ rule: 238,
214
+ dim: 245,
215
+ text: 252,
216
+ bright: 231,
217
+ accent: 39,
218
+ good: 46,
219
+ warn: 214,
220
+ tabOff: 240,
221
+ tabOn: 231
222
+ };
223
+ function render() {
224
+ const w = W(), h = H();
225
+ screen.fill({ attr: { bgColor: C.bg, color: C.text } });
226
+ clickables = [];
227
+ const title = " accelr8 ";
228
+ puts(0, 0, " ".repeat(w), { bgColor: C.panel, color: C.text });
229
+ puts(1, 0, title, { bgColor: C.accent, color: 16, bold: true });
230
+ puts(1 + title.length + 1, 0, "collaborative ascii canvas", { bgColor: C.panel, color: C.dim, italic: true });
231
+ const who = `${me.name} \u25CF`;
232
+ puts(w - who.length - 1, 0, who, { bgColor: C.panel, color: me.color, bold: true });
233
+ puts(0, 1, " ".repeat(w), { bgColor: C.bg });
234
+ let tx = 1;
235
+ const tabs = [["Projects", "projects"], ["Canvas", "canvas"], ["Members", "members"]];
236
+ for (const [label, scr] of tabs) {
237
+ const on = state.screen === scr;
238
+ const txt = ` ${label} `;
239
+ button(tx, 1, txt, {
240
+ bgColor: on ? C.accent : C.tabOff,
241
+ color: on ? 16 : C.text,
242
+ bold: on
243
+ }, () => setScreen(scr));
244
+ tx += txt.length + 1;
245
+ }
246
+ const dot = state.connected ? "\u25CF live" : "\u25CB offline";
247
+ puts(w - dot.length - 1, 1, dot, { bgColor: C.bg, color: state.connected ? C.good : C.warn });
248
+ puts(0, 2, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
249
+ if (state.screen === "projects") renderProjects();
250
+ else if (state.screen === "canvas") renderCanvas();
251
+ else renderMembers();
252
+ const sy = h - 1;
253
+ puts(0, sy, " ".repeat(w), { bgColor: C.panel });
254
+ const left = state.project ? ` ${state.project.name} \xB7 code ${state.project.code} \xB7 ${state.online.size} online ` : " no project open ";
255
+ puts(0, sy, left, { bgColor: C.panel, color: C.dim });
256
+ const hint = state.modal ? "[enter] ok [esc] cancel " : "[tab] switch [q] quit ";
257
+ puts(w - hint.length, sy, hint, { bgColor: C.panel, color: C.dim });
258
+ if (state.status) {
259
+ const s = " " + state.status + " ";
260
+ puts(Math.floor((w - s.length) / 2), sy, s, { bgColor: C.panel, color: C.warn });
261
+ }
262
+ if (state.modal) renderModal();
263
+ if (screen.dst) screen.draw({ delta: true });
264
+ }
265
+ function box(x0, y0, x1, y1, attr, label) {
266
+ puts(x0, y0, "\u250C" + "\u2500".repeat(Math.max(0, x1 - x0 - 1)) + "\u2510", attr);
267
+ puts(x0, y1, "\u2514" + "\u2500".repeat(Math.max(0, x1 - x0 - 1)) + "\u2518", attr);
268
+ for (let y = y0 + 1; y < y1; y++) {
269
+ puts(x0, y, "\u2502", attr);
270
+ puts(x1, y, "\u2502", attr);
271
+ }
272
+ if (label) puts(x0 + 2, y0, " " + label + " ", { ...attr, bold: true, color: C.bright });
273
+ }
274
+ function renderProjects() {
275
+ const w = W();
276
+ let y = 4;
277
+ if (w >= LOGO_W + 4 && H() >= LOGO.length + 16) {
278
+ for (const line of LOGO) {
279
+ puts(2, y, line, { color: C.accent, bgColor: C.bg });
280
+ y++;
281
+ }
282
+ y += 1;
283
+ }
284
+ puts(2, y, "PROJECTS", { color: C.bright, bold: true });
285
+ y += 1;
286
+ let bx = 2;
287
+ bx = button(bx, y, " + New Project ", { bgColor: C.good, color: 16, bold: true }, promptCreate) + 2;
288
+ button(bx, y, " Join by code ", { bgColor: C.accent, color: 16, bold: true }, promptJoin);
289
+ y += 2;
290
+ if (state.projects.length === 0) {
291
+ puts(2, y, "no projects yet. create one to start a shared canvas.", { color: C.dim, italic: true });
292
+ return;
293
+ }
294
+ puts(2, y, "CODE PROJECT MEMBERS ONLINE", { color: C.dim });
295
+ y += 1;
296
+ for (const p of state.projects) {
297
+ if (y >= H() - 2) break;
298
+ const row = `${p.code.padEnd(8)} ${p.name.slice(0, 30).padEnd(30)} ${String(p.members).padStart(5)} ${String(p.online).padStart(5)}`;
299
+ puts(2, y, "\u25BA", { color: C.accent });
300
+ button(4, y, row, { color: C.text, bgColor: C.bg }, () => sendMsg({ t: "joinProject", code: p.code }));
301
+ if (p.online > 0) puts(w - 3, y, "\u25CF", { color: C.good });
302
+ y++;
303
+ }
304
+ puts(2, H() - 3, "click a row to open its canvas", { color: C.dim, italic: true });
305
+ }
306
+ function renderCanvas() {
307
+ const w = W(), h = H();
308
+ if (!state.project) {
309
+ puts(2, 6, "no project open. go to the Projects tab and open or create one.", { color: C.dim, italic: true });
310
+ return;
311
+ }
312
+ const ty = 3;
313
+ puts(1, ty, "brush", { color: C.dim });
314
+ let px = 7;
315
+ for (const ch of PALETTE) {
316
+ const on = state.brush === ch;
317
+ button(px, ty, ` ${ch} `, {
318
+ bgColor: on ? C.bright : C.panel,
319
+ color: on ? 16 : C.text,
320
+ bold: on
321
+ }, () => {
322
+ state.brush = ch;
323
+ });
324
+ px += 3;
325
+ }
326
+ button(px + 1, ty, " \u232B erase ", {
327
+ bgColor: state.brush === " " ? C.bright : C.panel,
328
+ color: state.brush === " " ? 16 : C.text
329
+ }, () => {
330
+ state.brush = " ";
331
+ });
332
+ button(w - 10, ty, " clear ", { bgColor: C.warn, color: 16, bold: true }, () => sendMsg({ t: "clear" }));
333
+ const by0 = 4, by1 = h - 2;
334
+ box(0, by0, w - 1, by1, { color: C.rule, bgColor: C.bg }, `canvas ${CANVAS_W}\xD7${CANVAS_H}`);
335
+ const innerRight = w - 2, innerBottom = by1 - 1;
336
+ const clip = (sx, sy) => sx >= IO_X && sx <= innerRight && sy >= IO_Y && sy <= innerBottom;
337
+ for (const key in state.grid) {
338
+ const [cx, cy] = key.split(",").map(Number);
339
+ const sx = IO_X + cx, sy = IO_Y + cy;
340
+ if (clip(sx, sy)) puts(sx, sy, state.grid[key][0], { color: state.grid[key][1], bgColor: C.bg });
341
+ }
342
+ for (const [, c] of cursors) {
343
+ const sx = IO_X + Math.round(c.x), sy = IO_Y + Math.round(c.y);
344
+ if (!clip(sx, sy)) continue;
345
+ puts(sx, sy, "\u25B2", { color: c.color, bold: true });
346
+ const label = " " + c.name;
347
+ if (sx + 1 + label.length <= innerRight) puts(sx + 1, sy, label, { bgColor: c.color, color: 16, bold: true });
348
+ }
349
+ if (myLogical) {
350
+ const sx = IO_X + myLogical.x, sy = IO_Y + myLogical.y;
351
+ if (clip(sx, sy)) puts(sx, sy, state.brush === " " ? "\u25AB" : state.brush, { color: me.color, bold: true });
352
+ }
353
+ }
354
+ function copyToClipboard(text, tools) {
355
+ const candidates = tools ?? (process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]]);
356
+ const [tool, args] = candidates[0] ?? [null, []];
357
+ if (!tool) {
358
+ state.status = "copy failed (no clipboard tool)";
359
+ return;
360
+ }
361
+ try {
362
+ const p = spawn(tool, args, { stdio: ["pipe", "ignore", "ignore"] });
363
+ p.on("error", () => copyToClipboard(text, candidates.slice(1)));
364
+ p.on("close", (code) => {
365
+ state.status = code === 0 ? "copied join command" : "copy failed";
366
+ if (code !== 0 && candidates.length > 1) copyToClipboard(text, candidates.slice(1));
367
+ });
368
+ p.stdin.end(text);
369
+ } catch {
370
+ copyToClipboard(text, candidates.slice(1));
371
+ }
372
+ }
373
+ function renderMembers() {
374
+ if (!state.project) {
375
+ puts(2, 6, "no project open.", { color: C.dim, italic: true });
376
+ return;
377
+ }
378
+ puts(2, 4, "SHARE", { color: C.bright, bold: true });
379
+ puts(2, 6, "invite friends. they run", { color: C.dim });
380
+ const cmd = `accelr8 join ${state.project.code}`;
381
+ puts(2, 7, " " + cmd, { color: C.good, bold: true });
382
+ button(4 + cmd.length + 2, 7, " copy ", { bgColor: C.accent, color: 16, bold: true }, () => copyToClipboard(cmd));
383
+ puts(2, 9, "PROJECT CODE", { color: C.dim });
384
+ const codeBig = ` ${state.project.code.split("").join(" ")} `;
385
+ puts(2, 10, codeBig, { bgColor: C.bright, color: 16, bold: true });
386
+ puts(2, 12, `MEMBERS (${state.members.length})`, { color: C.bright, bold: true });
387
+ let y = 13;
388
+ for (const m of state.members) {
389
+ if (y >= H() - 2) break;
390
+ const on = state.online.has(m.id);
391
+ puts(2, y, on ? "\u25CF" : "\u25CB", { color: on ? C.good : C.dim });
392
+ puts(4, y, m.name + (m.id === me.id ? " (you)" : ""), { color: m.color, bold: true });
393
+ puts(24, y, on ? "online" : "offline", { color: on ? C.good : C.dim });
394
+ y++;
395
+ }
396
+ }
397
+ function renderModal() {
398
+ const m = state.modal;
399
+ const w = W(), h = H();
400
+ const bw = Math.min(60, w - 6), bh = 5;
401
+ const x0 = Math.floor((w - bw) / 2), y0 = Math.floor((h - bh) / 2);
402
+ for (let y = y0; y < y0 + bh; y++) puts(x0, y, " ".repeat(bw), { bgColor: C.panel });
403
+ box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, m.title);
404
+ const field = m.value + "\u258F";
405
+ puts(x0 + 2, y0 + 2, field.padEnd(bw - 4).slice(0, bw - 4), { bgColor: 236, color: C.bright });
406
+ }
407
+ function promptCreate() {
408
+ state.modal = { title: "New project name", value: "", onSubmit: (v) => {
409
+ if (v.trim()) sendMsg({ t: "createProject", name: v.trim() });
410
+ } };
411
+ }
412
+ function promptJoin() {
413
+ state.modal = { title: "Enter project code", value: "", onSubmit: (v) => {
414
+ if (v.trim()) sendMsg({ t: "joinProject", code: v.trim() });
415
+ } };
416
+ }
417
+ function onMouse(name, data) {
418
+ const mx = (data?.x ?? 1) - 1, my = (data?.y ?? 1) - 1;
419
+ if (state.modal) return;
420
+ if (name === "MOUSE_MOTION" || name === "MOUSE_DRAG") {
421
+ updateMyCursor(mx, my);
422
+ if (state.screen === "canvas" && state.project) {
423
+ if (name === "MOUSE_DRAG" ? data?.left : painting) paintAt(mx, my);
424
+ if (name === "MOUSE_DRAG" && data?.right) eraseAt(mx, my);
425
+ }
426
+ return;
427
+ }
428
+ if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
429
+ for (const r of clickables) {
430
+ if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
431
+ r.action();
432
+ return;
433
+ }
434
+ }
435
+ if (state.screen === "canvas" && state.project) {
436
+ painting = true;
437
+ lastPaintKey = "";
438
+ paintAt(mx, my);
439
+ }
440
+ return;
441
+ }
442
+ if (name === "MOUSE_LEFT_BUTTON_RELEASED") {
443
+ painting = false;
444
+ return;
445
+ }
446
+ if (name === "MOUSE_RIGHT_BUTTON_PRESSED") {
447
+ if (state.screen === "canvas" && state.project) eraseAt(mx, my);
448
+ }
449
+ }
450
+ function toLogical(mx, my) {
451
+ const cx = mx - IO_X, cy = my - IO_Y;
452
+ if (cx < 0 || cy < 0 || cx >= CANVAS_W || cy >= CANVAS_H) return null;
453
+ if (my < IO_Y || my > H() - 3) return null;
454
+ if (mx > W() - 2) return null;
455
+ return { x: cx, y: cy };
456
+ }
457
+ function updateMyCursor(mx, my) {
458
+ if (state.screen !== "canvas") {
459
+ myLogical = null;
460
+ return;
461
+ }
462
+ const lp = toLogical(mx, my);
463
+ myLogical = lp;
464
+ if (lp) cursorDirty = true;
465
+ }
466
+ function paintAt(mx, my) {
467
+ const lp = toLogical(mx, my);
468
+ if (!lp) return;
469
+ const key = `${lp.x},${lp.y}`;
470
+ if (key === lastPaintKey) return;
471
+ lastPaintKey = key;
472
+ const ch = state.brush;
473
+ if (ch === " ") delete state.grid[key];
474
+ else state.grid[key] = [ch, me.color];
475
+ sendMsg({ t: "paint", x: lp.x, y: lp.y, char: ch });
476
+ }
477
+ function eraseAt(mx, my) {
478
+ const lp = toLogical(mx, my);
479
+ if (!lp) return;
480
+ delete state.grid[`${lp.x},${lp.y}`];
481
+ sendMsg({ t: "paint", x: lp.x, y: lp.y, char: " " });
482
+ }
483
+ function setScreen(scr) {
484
+ state.screen = scr;
485
+ if (scr === "projects") sendMsg({ t: "listProjects" });
486
+ }
487
+ function onKey(name) {
488
+ if (name === "CTRL_C") return shutdown();
489
+ if (state.modal) {
490
+ const m = state.modal;
491
+ if (name === "ENTER") {
492
+ const v = m.value;
493
+ state.modal = null;
494
+ m.onSubmit(v);
495
+ } else if (name === "ESCAPE") state.modal = null;
496
+ else if (name === "BACKSPACE") m.value = m.value.slice(0, -1);
497
+ else if (name === "SPACE") m.value += " ";
498
+ else if (name.length === 1 && name >= " ") m.value += name;
499
+ return;
500
+ }
501
+ if (name === "q") return shutdown();
502
+ if (name === "TAB") {
503
+ const order = ["projects", "canvas", "members"];
504
+ setScreen(order[(order.indexOf(state.screen) + 1) % order.length]);
505
+ } else if (name === "1") setScreen("projects");
506
+ else if (name === "2") setScreen("canvas");
507
+ else if (name === "3") setScreen("members");
508
+ }
509
+ function startLoops() {
510
+ setInterval(() => {
511
+ for (const [id, c] of cursors) {
512
+ c.x += (c.tx - c.x) * 0.35;
513
+ c.y += (c.ty - c.y) * 0.35;
514
+ if (Date.now() - c.seen > 8e3) cursors.delete(id);
515
+ }
516
+ render();
517
+ }, 33);
518
+ setInterval(() => {
519
+ if (cursorDirty && myLogical && state.project) {
520
+ sendMsg({ t: "cursor", x: myLogical.x, y: myLogical.y });
521
+ cursorDirty = false;
522
+ }
523
+ }, 40);
524
+ setInterval(() => {
525
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
526
+ if (Date.now() - lastPong > 75e3) {
527
+ try {
528
+ ws.terminate();
529
+ } catch {
530
+ }
531
+ return;
532
+ }
533
+ try {
534
+ ws.ping();
535
+ } catch {
536
+ }
537
+ }, 3e4);
538
+ }
539
+ function restoreTerminal() {
540
+ try {
541
+ term.grabInput(false);
542
+ term.hideCursor(false);
543
+ term.fullscreen(false);
544
+ term.styleReset();
545
+ } catch {
546
+ }
547
+ }
548
+ function shutdown() {
549
+ restoreTerminal();
550
+ try {
551
+ ws?.close();
552
+ } catch {
553
+ }
554
+ process.exit(0);
555
+ }
556
+ function runClient(opts = {}) {
557
+ me = loadIdentity(opts.name);
558
+ SERVER = opts.server || process.env.ACCELR8_SERVER || DEFAULT_SERVER;
559
+ if (!/^wss?:\/\//.test(SERVER)) SERVER = "ws://" + SERVER;
560
+ try {
561
+ new URL(SERVER);
562
+ } catch {
563
+ console.error(`invalid server url: ${SERVER}`);
564
+ process.exit(1);
565
+ }
566
+ autoJoinCode = opts.joinCode;
567
+ const die = (err) => {
568
+ restoreTerminal();
569
+ console.error("accelr8 crashed:", err instanceof Error ? err.stack || err.message : err);
570
+ process.exit(1);
571
+ };
572
+ process.on("uncaughtException", die);
573
+ process.on("unhandledRejection", die);
574
+ term.fullscreen(true);
575
+ term.hideCursor();
576
+ term.grabInput({ mouse: "motion" });
577
+ makeScreen();
578
+ term.on("mouse", onMouse);
579
+ term.on("key", onKey);
580
+ term.on("resize", () => makeScreen());
581
+ process.on("SIGINT", shutdown);
582
+ process.on("SIGTERM", shutdown);
583
+ connect();
584
+ startLoops();
585
+ }
586
+ export {
587
+ __dump,
588
+ __setHeadlessScreen,
589
+ cursors,
590
+ render,
591
+ runClient,
592
+ state
593
+ };