// Shared TTY input utilities used by command handlers. // Provides a simple line-input "ask" helper and a reusable inline // raw-mode text editor (used by both commit message editing and // interactive config editing). import * as readline from "node:readline"; import { isStdinTTY } from "./tty"; // ── Simple line input (cooked mode) ────────────────────────────────── export function ask(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); }); } // ── Inline raw-mode editor ──────────────────────────────────────────── // // Provides a simple line editor that runs in raw mode and supports: // - Left/Right arrows, Home/End for cursor movement // - Backspace / Delete for character removal // - Ctrl+A (beginning), Ctrl+E (end), Ctrl+K (kill to end), Ctrl+U (kill to start) // - Enter to confirm, Ctrl+C / Esc to cancel // // Returns the edited string, or null if the user cancelled. export interface EditResult { value: string | null; } export async function editLine(initial: string): Promise { if (!isStdinTTY()) return null; const savedRaw = process.stdin.isRaw; process.stdin.setRawMode(true); process.stdin.resume(); let buffer = initial; let cursor = initial.length; function render() { // Clear line, move to start, show prompt + buffer, then reposition cursor process.stdout.write("\r\x1b[2K > " + buffer); if (cursor < buffer.length) { process.stdout.write(`\x1b[${buffer.length - cursor}D`); } } process.stdout.write(" > " + buffer); return new Promise((resolve) => { let escapeBuf = ""; function finish(value: string | null) { process.stdin.setRawMode(savedRaw === true); process.stdin.pause(); process.stdin.removeAllListeners("data"); process.stdout.write("\n"); resolve(value); } function handleEscapeSeq(seq: string) { switch (seq) { case "\x1b[D": case "\x1bOD": // Left if (cursor > 0) { cursor--; process.stdout.write("\x1b[D"); } break; case "\x1b[C": case "\x1bOC": // Right if (cursor < buffer.length) { cursor++; process.stdout.write("\x1b[C"); } break; case "\x1b[H": case "\x1b[1~": case "\x1bOH": // Home if (cursor > 0) { process.stdout.write(`\x1b[${cursor}D`); cursor = 0; } break; case "\x1b[F": case "\x1b[4~": case "\x1bOF": // End if (cursor < buffer.length) { process.stdout.write(`\x1b[${buffer.length - cursor}C`); cursor = buffer.length; } break; case "\x1b[3~": // Delete if (cursor < buffer.length) { buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1); render(); } break; } } process.stdin.on("data", (data: Buffer) => { const key = data.toString(); // Ctrl+C if (key === "\x03") { finish(null); return; } // Esc if (key === "\x1b") { if (escapeBuf) { // Already buffering — check if this completes a sequence const next = escapeBuf + key; if (/^(\x1b\[[0-9;]*[A-Za-z~]|\x1bO[A-Z])$/.test(next)) { handleEscapeSeq(next); escapeBuf = ""; } else { // Treat lone Esc as cancel finish(null); } return; } escapeBuf = "\x1b"; return; } // Buffering an escape sequence if (escapeBuf) { escapeBuf += key; // Check if this completes a valid sequence if (/^(\x1b\[[0-9;]*[A-Za-z~]|\x1bO[A-Z])$/.test(escapeBuf)) { handleEscapeSeq(escapeBuf); escapeBuf = ""; } else if (escapeBuf.length > 10 || /^[A-Za-z~]$/.test(key)) { // Timeout or terminator that didn't match — discard escapeBuf = ""; } return; } // Enter if (key === "\r" || key === "\n") { const result = buffer.trim(); finish(result || null); return; } // Backspace if (key === "\x7f") { if (cursor > 0) { buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor); cursor--; render(); } return; } // Ctrl+A → beginning of line if (key === "\x01") { if (cursor > 0) { process.stdout.write(`\x1b[${cursor}D`); cursor = 0; } return; } // Ctrl+E → end of line if (key === "\x05") { if (cursor < buffer.length) { process.stdout.write(`\x1b[${buffer.length - cursor}C`); cursor = buffer.length; } return; } // Ctrl+K → kill to end if (key === "\x0b") { buffer = buffer.slice(0, cursor); render(); return; } // Ctrl+U → kill to start if (key === "\x15") { buffer = buffer.slice(cursor); cursor = 0; render(); return; } // Printable characters if (key >= " " && key !== "\x7f") { buffer = buffer.slice(0, cursor) + key + buffer.slice(cursor); cursor += key.length; render(); } }); }); }