Keep terminal pairing QR codes scannable (#2381)
* fix(pairing): keep terminal QR codes scannable Prompt framing could wrap dense QR codes and alter long pairing links. Print pairing instructions directly, add the standard quiet zone, and suppress codes that cannot fit without auto-wrapping. * fix(pairing): contain terminal QR background
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as QRCode from "qrcode";
|
||||
import { renderPairingQr } from "./pairing-qr.js";
|
||||
|
||||
const ESCAPE = String.fromCharCode(0x1b);
|
||||
const ANSI_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
||||
|
||||
describe("renderPairingQr", () => {
|
||||
it("renders a theme-independent QR code with a four-module quiet zone", async () => {
|
||||
const url = "https://app.paseo.sh/#offer=test-pairing-offer";
|
||||
const qr = await renderPairingQr(url);
|
||||
const styledLines = qr.split("\n");
|
||||
const visibleLines = qr.replace(ANSI_PATTERN, "").split("\n");
|
||||
const moduleCount = QRCode.create(url).modules.size;
|
||||
|
||||
expect(
|
||||
styledLines.every(
|
||||
(line) => line.startsWith(`${ESCAPE}[47m${ESCAPE}[30m`) && line.endsWith(`${ESCAPE}[0m`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(visibleLines.every((line) => line.length === moduleCount + 8)).toBe(true);
|
||||
expect(visibleLines.slice(0, 2).every((line) => line.trim() === "")).toBe(true);
|
||||
expect(visibleLines.slice(-2).every((line) => line.trim() === "")).toBe(true);
|
||||
expect(visibleLines.every((line) => line.startsWith(" ") && line.endsWith(" "))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +1,18 @@
|
||||
import * as QRCode from "qrcode";
|
||||
import type { QRCodeToStringOptionsTerminal, QRCodeToStringOptionsOther } from "qrcode";
|
||||
import type { Logger } from "pino";
|
||||
import type { QRCodeToStringOptionsOther } from "qrcode";
|
||||
|
||||
function parseBooleanEnv(value: string | undefined): boolean | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldPrintPairingQr(): boolean {
|
||||
const env = parseBooleanEnv(process.env.PASEO_PAIRING_QR);
|
||||
if (env !== undefined) return env;
|
||||
return process.stdout.isTTY ?? false;
|
||||
}
|
||||
const BLACK_ON_WHITE = "\u001b[47m\u001b[30m";
|
||||
const RESET_COLORS = "\u001b[0m";
|
||||
|
||||
export async function renderPairingQr(url: string): Promise<string> {
|
||||
const terminalOptions: QRCodeToStringOptionsTerminal = {
|
||||
type: "terminal",
|
||||
small: true,
|
||||
};
|
||||
|
||||
const utf8Options: QRCodeToStringOptionsOther = {
|
||||
type: "utf8",
|
||||
margin: 4,
|
||||
};
|
||||
|
||||
try {
|
||||
return await QRCode.toString(url, terminalOptions);
|
||||
} catch {
|
||||
return await QRCode.toString(url, utf8Options);
|
||||
}
|
||||
}
|
||||
|
||||
export async function printPairingQrIfEnabled(args: {
|
||||
url: string;
|
||||
logger?: Logger;
|
||||
}): Promise<void> {
|
||||
if (!shouldPrintPairingQr()) return;
|
||||
|
||||
const qr = await renderPairingQr(args.url);
|
||||
const out = `\nScan to pair:\n${qr}\n${args.url}\n`;
|
||||
|
||||
try {
|
||||
process.stdout.write(out);
|
||||
} catch (error) {
|
||||
args.logger?.debug({ error }, "Failed to print pairing QR");
|
||||
}
|
||||
const qr = await QRCode.toString(url, utf8Options);
|
||||
return qr
|
||||
.split("\n")
|
||||
.map((line) => `${BLACK_ON_WHITE}${line}${RESET_COLORS}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user