import type { FileEntry } from "./types"; import { BOLD, GREEN, YELLOW, CYAN, DIM, RESET } from "./terminal"; const UP = "\x1b[A"; const DOWN = "\x1b[B"; const SPACE = " "; const ENTER = "\r"; const CTRL_C = "\x03"; function hideCursor() { process.stdout.write("\x1b[?25l"); } function showCursor() { process.stdout.write("\x1b[?25h"); } function moveUp(n: number) { process.stdout.write(`\x1b[${n}A`); } function moveDown(n: number) { process.stdout.write(`\x1b[${n}B`); } function clearLine() { process.stdout.write("\x1b[2K\r"); } interface SelectItem { label: string; path: string; selected: boolean; } export async function selectFiles( stagedFiles: FileEntry[], unstagedFiles: FileEntry[], ): Promise { if (unstagedFiles.length === 0) return []; if (stagedFiles.length > 0) { process.stdout.write(`\n ${BOLD}Staged files (will be included):${RESET}\n`); for (const f of stagedFiles) { process.stdout.write(` ${GREEN}✓${RESET} ${f.path} (${YELLOW}${f.label}${RESET})\n`); } } const items: SelectItem[] = [ { label: "Select all", path: "__all__", selected: false }, ...unstagedFiles.map((f) => ({ label: `${f.path} (${f.label})`, path: f.path, selected: false, })), ]; let cursor = 0; process.stdout.write(`\n ${BOLD}Select files to stage:${RESET}\n`); process.stdout.write(` ${DIM}↑/↓ navigate, space select, enter confirm${RESET}\n\n`); const itemStartRow = 4 + (stagedFiles.length > 0 ? stagedFiles.length + 2 : 0); function render() { for (let i = 0; i < items.length; i++) { process.stdout.write("\x1b[2K\r"); const item = items[i]!; const isAll = i === 0; const cursor_ = i === cursor ? `${CYAN}❯${RESET} ` : " "; const checkbox = item.selected ? `${GREEN}◉${RESET}` : `${DIM}○${RESET}`; if (isAll) { process.stdout.write(`${cursor_} ${checkbox} ${BOLD}${item.label}${RESET}\n`); } else { process.stdout.write(`${cursor_} ${checkbox} ${item.path.includes("(") ? item.label : `${item.label}`}\n`); } } moveUp(items.length); } function toggleItem(index: number) { const item = items[index]!; item.selected = !item.selected; if (index === 0) { for (let i = 1; i < items.length; i++) { items[i]!.selected = item.selected; } } else { const allSelected = items.slice(1).every((it) => it.selected); items[0]!.selected = allSelected; } } return new Promise((resolve) => { if (process.stdin.isTTY !== true) { resolve([]); return; } const wasRaw = process.stdin.isRaw; if (wasRaw !== true) { process.stdin.setRawMode(true); } process.stdin.resume(); hideCursor(); render(); const onData = (data: Buffer) => { const key = data.toString(); if (key === CTRL_C) { process.stdin.setRawMode(wasRaw === true); process.stdin.pause(); process.stdin.removeListener("data", onData); showCursor(); for (let i = 0; i < items.length; i++) { process.stdout.write("\x1b[2K\n"); } moveUp(items.length); process.stdout.write(`\n Aborted.\n`); process.exit(1); } if (key === UP) { if (cursor > 0) { cursor--; render(); } } else if (key === DOWN) { if (cursor < items.length - 1) { cursor++; render(); } } else if (key === SPACE) { toggleItem(cursor); render(); } else if (key === ENTER) { process.stdin.setRawMode(wasRaw === true); process.stdin.pause(); process.stdin.removeListener("data", onData); for (let i = 0; i < items.length; i++) { process.stdout.write("\x1b[2K\n"); } moveUp(items.length); const selected = items .slice(1) .filter((it) => it.selected) .map((it) => it.path); if (selected.length > 0) { process.stdout.write( ` ${GREEN}Staged ${selected.length} file(s):${RESET} ${selected.join(", ")}\n`, ); } showCursor(); resolve(selected); } }; process.stdin.on("data", onData); }); }