From 5ae23442730088ab7fc5d60bd9bb00744ab421de Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 10 Sep 2026 08:37:37 -0400 Subject: [PATCH] Scan a short address, not a command line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `prompt.ac/~` instead of `aesthetic.computer/prompt~channel%20~!autorun`. The host redirects one to the other, so the phone still lands on the prompt holding the channel as its own word — that rule did not go away, it moved into lith/Caddyfile where the expansion happens. What it buys: 27 bytes with the scheme a camera needs, against 61 before. That is a version-2 symbol rather than version 4 — 25 modules instead of 33 — so every module is a third wider on the rock, which is the surface someone is actually holding a phone up to. It reads like an address now, too, which the old one never did. The size guard in the tests measures the URL a phone actually gets, `https://` included, so lengthening the address fails there rather than quietly costing a version. --- src/live.mjs | 13 +++++++++---- test/live.test.mjs | 40 +++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/live.mjs b/src/live.mjs index 016268d586..d2508ef719 100644 --- a/src/live.mjs +++ b/src/live.mjs @@ -27,7 +27,7 @@ import { DEFAULT_RUNTIME, runtimeFor, runtimeForExtension } from "./runtimes.mjs export const SITE = "https://aesthetic.computer"; // Scannable codes get smaller the shorter the text, and this address exists to -// be scanned. prompt.ac is the same runtime as aesthetic.computer behind a +// be scanned — see `scanUrl` for the short route it uses. prompt.ac is the same runtime as aesthetic.computer behind a // leaner shell, and nine bytes shorter — which is a whole version of the code // once the scheme a phone camera needs is added back on. export const SCAN_HOST = "prompt.ac"; @@ -76,10 +76,15 @@ export class LivePiece extends EventEmitter { } // The URL a phone scans: it joins the code channel and then sits waiting for - // source, which arrives as soon as anything is pushed. See the note at the - // top of the file for why the argument is separated by an encoded space. + // source, which arrives as soon as anything is pushed. + // + // Short on purpose. Every byte here is a module of QR, and the long form this + // expands to — `/prompt~channel%20~!autorun`, a command line wearing + // percent-encoding — costs a whole extra version of the symbol: smaller + // modules, on a surface someone is holding a phone up to. The `~` + // route on the host redirects to it, so the code stays at version 2. get scanUrl() { - return `${this.scanHost}/prompt~channel%20${this.channel}~!autorun`; + return `${this.scanHost}/~${this.channel}`; } // Where the piece answers once it has been published under a handle. diff --git a/test/live.test.mjs b/test/live.test.mjs index d007125b45..128fbc6f6e 100644 --- a/test/live.test.mjs +++ b/test/live.test.mjs @@ -22,28 +22,34 @@ test("names are pronounceable and channels stay short enough to scan", () => { assert.match(randomSlug(), /^[a-z]{4,6}$/); assert.match(randomChannel(), /^[A-Za-z0-9_-]{8}$/); } - // A version-3 QR is the largest that fits a terminal corner: 33 columns. - // Measure the real scan URL rather than a copy of it, so lengthening the URL - // fails here instead of quietly growing the code to a version that no longer - // fits an 80×24 window and is dropped from the frame. + // The address is short so the code can be. Measure the real scan URL rather + // than a copy of it, so lengthening it fails here instead of quietly growing + // the symbol a version — which costs modules on the rock, where someone is + // holding a phone up to it. A phone gets the URL with `https://` in front, + // so that is what is measured: 32 bytes is a version-2 code at level L. for (let attempt = 0; attempt < 50; attempt += 1) { const url = new LivePiece({ cwd: tmpdir() }).scanUrl; - assert.ok(Buffer.byteLength(url) <= 53, `${url} is too long for a version-3 code`); + const scanned = `https://${url}`; + assert.ok( + Buffer.byteLength(scanned) <= 32, + `${scanned} is too long for a version-2 code`, + ); const block = qrBlock(url); - assert.equal(block.width, 33); - assert.equal(block.height, 17); + assert.equal(block.width, 29); + assert.equal(block.height, 15); } }); -// The phone has to land on the prompt holding the channel as its own word. -// parse.mjs hands everything after `prompt~` to the prompt as ONE parameter, -// and the prompt splits its own arguments on spaces — a tilde glues the channel -// to the command name, so `channel` runs with no argument and joins nothing. -test("the scan URL separates the channel from the command with a space", () => { +// The scanned address is the short one; the host expands it. The rule it +// expands to still matters — parse.mjs hands everything after `prompt~` to the +// prompt as ONE parameter and the prompt splits its own arguments on spaces, so +// the channel must arrive as its own word — but that lives in the `~` +// redirect in lith/Caddyfile now, not here. +test("the scan URL is the short route, not the command line", () => { const live = new LivePiece({ cwd: tmpdir(), channel: "Ab0-_9Zz" }); - assert.equal(live.scanUrl, "prompt.ac/prompt~channel%20Ab0-_9Zz~!autorun"); - const [, command] = live.scanUrl.match(/\/prompt~(.*)~!autorun$/); - assert.deepEqual(decodeURIComponent(command).split(" "), ["channel", "Ab0-_9Zz"]); + assert.equal(live.scanUrl, "prompt.ac/~Ab0-_9Zz"); + const [, channel] = live.scanUrl.match(/\/~(.+)$/); + assert.equal(channel, "Ab0-_9Zz", "the channel travels whole, needing no encoding"); }); // Read the rendered block back into a grid of dark modules. A code that is @@ -67,7 +73,7 @@ function decodeBlock(block) { test("the rendered code matches the encoder module for module", async () => { const { qrcode, ErrorCorrectLevel } = await import("../src/vendor/qr.mjs"); - const url = "prompt.ac/prompt~channel%20Ab0-_9Zz~!autorun"; + const url = "prompt.ac/~Ab0-_9Zz"; const quiet = 2; const modules = qrcode(url, { errorCorrectLevel: ErrorCorrectLevel.L }).modules; const grid = decodeBlock(qrBlock(url, { quiet })); @@ -154,7 +160,7 @@ test("a session mints a blank piece, pushes it, and cleans up after itself", asy }); assert.equal(live.file, join(root, "movika.mjs")); - assert.equal(live.scanUrl, "prompt.ac/prompt~channel%20Ab0-_9Zz~!autorun"); + assert.equal(live.scanUrl, "prompt.ac/~Ab0-_9Zz"); assert.equal(live.publishedUrl("jeffrey"), "https://aesthetic.computer/@jeffrey/movika"); assert.equal(live.publishedUrl(""), ""); -- 2.51.2