moshcode 0.72.0 → 0.73.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.
- package/package.json +1 -1
- package/src/mirror.mjs +55 -1
- package/src/tui.mjs +8 -1
package/package.json
CHANGED
package/src/mirror.mjs
CHANGED
|
@@ -12,6 +12,47 @@
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import { loadCreds } from "./auth.mjs";
|
|
14
14
|
|
|
15
|
+
// A key pressed on the session page travels through the same queue as a typed
|
|
16
|
+
// line, tagged with this sentinel. ESC leads it because it is a byte you cannot
|
|
17
|
+
// put in the send box by typing, so a key can never collide with a real command
|
|
18
|
+
// — and the app refuses to queue one for a mosh too old to decode it, rather
|
|
19
|
+
// than letting the sentinel get typed at somebody's prompt as text.
|
|
20
|
+
export const KEY_PREFIX = "\u001bmoshkey:";
|
|
21
|
+
/** Keys the page can send. Anything else is ignored on both ends. */
|
|
22
|
+
export const KEY_NAMES = ["up", "down", "left", "right", "enter"];
|
|
23
|
+
|
|
24
|
+
/** The name of the key this command carries, or null if it is an ordinary line. */
|
|
25
|
+
export function decodeKey(body) {
|
|
26
|
+
if (typeof body !== "string" || !body.startsWith(KEY_PREFIX)) return null;
|
|
27
|
+
const name = body.slice(KEY_PREFIX.length);
|
|
28
|
+
return KEY_NAMES.includes(name) ? name : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// What each key looks like to a program reading the tty in raw mode, and the
|
|
32
|
+
// keypress readline wants when it is the one holding the line.
|
|
33
|
+
const KEY_BYTES = { up: "\u001b[A", down: "\u001b[B", right: "\u001b[C", left: "\u001b[D", enter: "\r" };
|
|
34
|
+
const KEY_PRESS = {
|
|
35
|
+
up: { name: "up" }, down: { name: "down" }, right: { name: "right" },
|
|
36
|
+
left: { name: "left" }, enter: { name: "return" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Deliver one key to whatever is reading this terminal. Returns false for a key
|
|
41
|
+
* we don't know, so a newer app can never make an older CLI do something odd.
|
|
42
|
+
*/
|
|
43
|
+
export function pressKey(name, rl = null, stdin = process.stdin) {
|
|
44
|
+
const bytes = KEY_BYTES[name];
|
|
45
|
+
if (!bytes) return false;
|
|
46
|
+
// At the prompt readline owns the line editor, so hand it a keypress rather
|
|
47
|
+
// than bytes: ↑/↓ walk the history, ←/→ move within the line, enter runs it.
|
|
48
|
+
if (rl) {
|
|
49
|
+
try { rl.write(null, KEY_PRESS[name]); return true; } catch { /* fall through to the tty */ }
|
|
50
|
+
}
|
|
51
|
+
// Otherwise something has the tty in raw mode — a herd bar, the reader, a
|
|
52
|
+
// menu — and it is waiting on the real escape sequence, not on readline.
|
|
53
|
+
try { stdin.emit("data", Buffer.from(bytes, "latin1")); return true; } catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
15
56
|
const FLUSH_MS = 150; // batch writes so a busy render is one request, not fifty
|
|
16
57
|
const MAX_BUFFER = 16000; // flush early once a batch gets big
|
|
17
58
|
|
|
@@ -32,6 +73,7 @@ export function createMirror({
|
|
|
32
73
|
// process alive for up to a full poll window after the pit closes.
|
|
33
74
|
let poll = null;
|
|
34
75
|
const onCommand = new Set();
|
|
76
|
+
const onKey = new Set();
|
|
35
77
|
|
|
36
78
|
const api = (creds?.api || "https://app.moshcode.sh").replace(/\/+$/, "");
|
|
37
79
|
const headers = { "content-type": "application/json", authorization: `Bearer ${creds?.token}` };
|
|
@@ -114,7 +156,13 @@ export function createMirror({
|
|
|
114
156
|
if (stopped) return;
|
|
115
157
|
if (!got) { await sleep(5000); continue; }
|
|
116
158
|
for (const c of got.commands || []) {
|
|
117
|
-
|
|
159
|
+
// A key is a navigation action, not a line, so it goes to its own
|
|
160
|
+
// handlers: the pit presses it the moment it lands instead of parking
|
|
161
|
+
// it behind whatever text is still waiting for the prompt. A "down"
|
|
162
|
+
// delivered a command later would land on a different row.
|
|
163
|
+
const key = decodeKey(c.body);
|
|
164
|
+
if (key) { for (const fn of onKey) { try { fn(key); } catch { /* handler's problem */ } } }
|
|
165
|
+
else { for (const fn of onCommand) { try { fn(c.body); } catch { /* handler's problem */ } } }
|
|
118
166
|
post(`/api/sessions/${sessionId}/commands/${c.id}`, {});
|
|
119
167
|
}
|
|
120
168
|
}
|
|
@@ -131,6 +179,10 @@ export function createMirror({
|
|
|
131
179
|
host: os.hostname(),
|
|
132
180
|
version,
|
|
133
181
|
cwd,
|
|
182
|
+
// What this build can be asked to do. The page arms its arrow pad on
|
|
183
|
+
// the strength of this: a mosh that never says "keys" is one that would
|
|
184
|
+
// type the sentinel at the prompt instead of pressing it.
|
|
185
|
+
features: ["keys"],
|
|
134
186
|
...size(),
|
|
135
187
|
});
|
|
136
188
|
if (!r?.id) return false;
|
|
@@ -146,6 +198,8 @@ export function createMirror({
|
|
|
146
198
|
setEngine,
|
|
147
199
|
/** Subscribe to commands sent from the web. Returns an unsubscribe fn. */
|
|
148
200
|
onCommand(fn) { onCommand.add(fn); return () => onCommand.delete(fn); },
|
|
201
|
+
/** Subscribe to keys pressed on the web. Returns an unsubscribe fn. */
|
|
202
|
+
onKey(fn) { onKey.add(fn); return () => onKey.delete(fn); },
|
|
149
203
|
async stop() {
|
|
150
204
|
if (!sessionId || stopped) return;
|
|
151
205
|
stopped = true;
|
package/src/tui.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { locate, tilde } from "./pwd.mjs";
|
|
|
17
17
|
import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
|
|
18
18
|
import { loginAuto, whoami, logout } from "./auth.mjs";
|
|
19
19
|
import { loadCommand, saveCommand } from "./settings-sync.mjs";
|
|
20
|
-
import { createMirror, teeOutput } from "./mirror.mjs";
|
|
20
|
+
import { createMirror, pressKey, teeOutput } from "./mirror.mjs";
|
|
21
21
|
import { fetchMotdAd } from "./ads.mjs";
|
|
22
22
|
import { runScript } from "./runtime.mjs";
|
|
23
23
|
import { moshVocabulary } from "./commands.mjs";
|
|
@@ -1302,6 +1302,13 @@ async function startMirror() {
|
|
|
1302
1302
|
};
|
|
1303
1303
|
mirror.onCommand((body) => { queue.push(body); drainRemote(); });
|
|
1304
1304
|
|
|
1305
|
+
// Keys skip the queue: they are pressed the instant they arrive, whether the
|
|
1306
|
+
// prompt is armed or something else has the tty (a herd bar, the reader, a
|
|
1307
|
+
// menu). Nothing is echoed for them either — a line gets a `▸ (web)` note
|
|
1308
|
+
// because it would otherwise appear from nowhere, but a key's effect is the
|
|
1309
|
+
// redraw it causes, and printing over that would shift it out of place.
|
|
1310
|
+
mirror.onKey((name) => { pressKey(name, promptRl); });
|
|
1311
|
+
|
|
1305
1312
|
console.log(info(`mirroring this session → ${acid(mirror.url)}`));
|
|
1306
1313
|
return { restoreTee, drainRemote, atPrompt: (rl) => { promptRl = rl; } };
|
|
1307
1314
|
}
|