refactor: deduplicate pickToTTY as wrapper around pickToTTYWithActions (#1891)

Fixes #1890

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-24 14:48:57 -08:00 committed by GitHub
parent fda44b10f7
commit d423ea57d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 3 additions and 189 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.10.6",
"version": "0.10.7",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -124,194 +124,8 @@ function getTTYCols(ttyFd: number): number {
* fd) but returns void so callers can `await` it uniformly.
*/
export function pickToTTY(config: PickConfig): string | null {
if (config.options.length === 0) {
return config.defaultValue ?? null;
}
// ── open /dev/tty ──────────────────────────────────────────────────────────
let ttyFd: number;
try {
ttyFd = fs.openSync("/dev/tty", "r+");
} catch {
return pickFallback(config);
}
// ── save terminal settings ────────────────────────────────────────────────
const savedRes = spawnSync(
"stty",
[
"-g",
],
{
stdio: [
ttyFd,
"pipe",
"pipe",
],
},
);
if (savedRes.status !== 0 || !savedRes.stdout) {
fs.closeSync(ttyFd);
return pickFallback(config);
}
const savedSettings = savedRes.stdout.toString().trim();
// ── enable raw / no-echo mode ─────────────────────────────────────────────
const rawRes = spawnSync(
"stty",
[
"raw",
"-echo",
],
{
stdio: [
ttyFd,
"pipe",
"pipe",
],
},
);
if (rawRes.status !== 0) {
fs.closeSync(ttyFd);
return pickFallback(config);
}
// ── helpers ───────────────────────────────────────────────────────────────
const w = (s: string) => {
try {
fs.writeSync(ttyFd, s);
} catch {}
};
const restore = () => {
try {
spawnSync(
"stty",
[
savedSettings,
],
{
stdio: [
ttyFd,
"pipe",
"pipe",
],
},
);
} catch {}
w(A.showC);
try {
fs.closeSync(ttyFd);
} catch {}
};
// ── initial state ─────────────────────────────────────────────────────────
let selected = 0;
if (config.defaultValue) {
const idx = config.options.findIndex((o) => o.value === config.defaultValue);
if (idx >= 0) {
selected = idx;
}
}
const cols = getTTYCols(ttyFd);
const maxW = cols - 1; // leave 1 char margin to prevent terminal auto-wrap
// header line + one line per option + footer line
const pickerHeight = config.options.length + 2;
const render = (first: boolean) => {
if (!first) {
w(A.up(pickerHeight) + A.col1 + A.clearBelow);
}
w(`${A.bold}${A.cyan}? ${trunc(config.message, maxW - 2)}${A.reset}\r\n`);
for (let i = 0; i < config.options.length; i++) {
const opt = config.options[i];
if (i === selected) {
const label = trunc(opt.label, maxW - 2);
w(`${A.green}${A.bold}> ${label}${A.reset}`);
if (opt.hint) {
const remaining = maxW - 2 - label.length - 2;
if (remaining > 3) {
w(` ${A.dim}${trunc(opt.hint, remaining)}${A.reset}`);
}
}
} else {
w(` ${A.dim}${trunc(opt.label, maxW - 2)}${A.reset}`);
}
w("\r\n");
}
w(`${A.dim} ${trunc("\u2191/\u2193 move \u23ce select Ctrl-C cancel", maxW - 2)}${A.reset}\r\n`);
};
// ── render & key loop ─────────────────────────────────────────────────────
w(A.hideC);
render(true);
const buf = Buffer.alloc(8);
let result: string | null = null;
try {
// Synchronous blocking read loop — each iteration waits for one keypress.
// Arrow keys (\x1b[A / \x1b[B) arrive as a single read() because the
// terminal driver delivers escape sequences atomically.
outer: while (true) {
let n: number;
try {
n = fs.readSync(ttyFd, buf, 0, 8);
} catch {
break;
}
if (n === 0) {
continue;
}
const key = buf.slice(0, n).toString("binary");
switch (key) {
// ── cancel ─────────────────────────────────────────────────────────
case "\x03": // Ctrl-C
case "\x1b": // standalone Escape
break outer;
// ── confirm ────────────────────────────────────────────────────────
case "\r":
case "\n": {
result = config.options[selected].value;
// Replace picker with a one-line confirmation
w(A.up(pickerHeight) + A.col1 + A.clearBelow);
const opt = config.options[selected];
w(
`${A.green}${A.bold}> ${config.message}:${A.reset} ` +
`${A.cyan}${trunc(opt.label, maxW - config.message.length - 4)}${A.reset}\r\n`,
);
break outer;
}
// ── navigation ─────────────────────────────────────────────────────
case "\x1b[A": // Up (CSI)
case "\x1bOA": // Up (SS3, some terminals)
case "k": // vim-style
selected = (selected - 1 + config.options.length) % config.options.length;
render(false);
break;
case "\x1b[B": // Down (CSI)
case "\x1bOB": // Down (SS3)
case "j": // vim-style
selected = (selected + 1) % config.options.length;
render(false);
break;
default:
break;
}
}
} finally {
restore();
}
return result;
const result = pickToTTYWithActions(config);
return result.action === "select" ? result.value : null;
}
/**