/** * A QR code as terminal lines or an inline SVG. * * Uses the `qrcode` package only for the module matrix, then draws Unicode * half-blocks for a terminal and hands the package's SVG to a page. The package * loads on the first QR, not when an extension registers: a session that never * shows one pays nothing for it. No ANSI is emitted; the caller decides colours, * and the width is the matrix width plus a quiet zone. * * Two surfaces share this: `wechat` shows a login QR, and `plan` shows a review * link a teammate can scan. */ interface QrModules { size: number; get(row: number, col: number): boolean; } /** QR error-correction level, one setting for both surfaces. */ const ERROR_CORRECTION = "M" as const; /** Build the module matrix for `text`, loading `qrcode` on first use. */ export async function qrMatrix(text: string): Promise { const { default: QRCode } = await import("qrcode"); const qr = QRCode.create(text, { errorCorrectionLevel: ERROR_CORRECTION }); return qr.modules as unknown as QrModules; } /** * The QR as terminal lines. Each string is exactly `matrix.size + 2 * quiet` * columns wide (half-block characters are single width), so a caller can test * `qrLines(text)[0].length` against the terminal width. */ export async function qrLines(text: string, options: { quiet?: number } = {}): Promise { const quiet = Math.max(0, options.quiet ?? 2); const matrix = await qrMatrix(text); const size = matrix.size; const isDark = (row: number, col: number): boolean => row >= 0 && col >= 0 && row < size && col < size && matrix.get(row, col); const lines: string[] = []; for (let y = -quiet; y < size + quiet; y += 2) { let line = ""; for (let x = -quiet; x < size + quiet; x += 1) { const top = isDark(y, x); const bottom = isDark(y + 1, x); line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " "; } lines.push(line); } return lines; } /** * The QR as an inline SVG, for a page that can paste markup instead of pixels. * * The markup is generated by the package from a URL this process built, so it * carries no caller-supplied text; a page still serves it under its own CSP, * which allows no external image. */ export async function qrSvg(text: string): Promise { const { default: QRCode } = await import("qrcode"); return QRCode.toString(text, { type: "svg", errorCorrectionLevel: ERROR_CORRECTION, margin: 1 }); }