fix(cli): add quiet zone around terminal QR; fix q-key quit via /dev/tty

- renderToTerminal now pads with a 4-module quiet zone (white border)
  horizontally and vertically, matching the webapp rasterizer.
- Keyboard input now tries /dev/tty first so q/Ctrl-C works even when
  stdin is a pipe (e.g. echo ... | bun run src/cli/qr-terminal.ts).
- Update all CLI renderer tests for padded output; add tests for custom
  and zero quiet zone.
- Increase outer_ec_benefit test timeout to 15s to avoid flaky failures.
- Rebuild CLI bundle and webapp; redeploy to nginx.
This commit is contained in:
Hermes Agent
2026-05-03 21:38:17 +00:00
parent 5340ae8fe1
commit cdb43f36db
4 changed files with 110 additions and 39 deletions
+28 -8
View File
@@ -13,7 +13,8 @@
* node --import=tsx src/cli/qr-terminal.ts ... # with tsx
*/
import { readFileSync, existsSync } from 'fs';
import { readFileSync, existsSync, openSync, closeSync } from 'fs';
import { ReadStream } from 'tty';
import { generateQRMatrix } from '../core/qr/qr_encode';
import { packetize } from '../core/sender/packetizer';
import { scheduleFrames } from '../core/sender/scheduler';
@@ -141,21 +142,40 @@ function main() {
clearInterval(interval);
clearScreen();
showCursor();
if (ttyFd !== null) {
try { closeSync(ttyFd); } catch {}
}
process.stdout.write('QR terminal display stopped.\n');
process.exit(0);
}
// Keyboard handling
if (process.stdin.isTTY) {
hideCursor();
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', (key: string) => {
// Keyboard handling — try /dev/tty first so it works even when stdin is a pipe
let ttyFd: number | null = null;
try {
ttyFd = openSync('/dev/tty', 'rs');
const stream = new ReadStream(ttyFd);
stream.setRawMode(true);
stream.setEncoding('utf8');
stream.on('data', (key: string) => {
if (key === 'q' || key === 'Q' || key === '\u0003') {
cleanup();
}
});
stream.resume();
hideCursor();
} catch {
ttyFd = null;
if (process.stdin.isTTY) {
hideCursor();
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', (key: string) => {
if (key === 'q' || key === 'Q' || key === '\u0003') {
cleanup();
}
});
}
}
process.on('SIGINT', cleanup);
+16 -2
View File
@@ -15,14 +15,22 @@ const BLOCK_EMPTY = ' ';
/**
* Render a QR boolean matrix to terminal lines.
* @param matrix 2-D array where true = dark module
* @param quietZone number of white modules to pad on each side (default 4)
* @returns Array of terminal strings (one per screen row)
*/
export function renderToTerminal(matrix: boolean[][]): string[] {
export function renderToTerminal(matrix: boolean[][], quietZone: number = 4): string[] {
const size = matrix.length;
const totalWidth = size + quietZone * 2;
const lines: string[] = [];
// Top quiet zone — each terminal row covers 2 QR modules vertically
const padRows = Math.ceil(quietZone / 2);
for (let i = 0; i < padRows; i++) {
lines.push(' '.repeat(totalWidth));
}
for (let y = 0; y < size; y += 2) {
let line = '';
let line = ' '.repeat(quietZone);
for (let x = 0; x < size; x++) {
const top = matrix[y][x];
const bottom = y + 1 < size ? matrix[y + 1][x] : false;
@@ -37,9 +45,15 @@ export function renderToTerminal(matrix: boolean[][]): string[] {
line += BLOCK_EMPTY;
}
}
line += ' '.repeat(quietZone);
lines.push(line);
}
// Bottom quiet zone
for (let i = 0; i < padRows; i++) {
lines.push(' '.repeat(totalWidth));
}
return lines;
}