(question: string, choices: string[])
| 87 | } |
| 88 | |
| 89 | function promptUser(question: string, choices: string[]): Promise<number> { |
| 90 | return new Promise((resolve) => { |
| 91 | let selected = 0 |
| 92 | |
| 93 | const render = () => { |
| 94 | // Move cursor up to rewrite choices (skip on first draw) |
| 95 | if (rendered) process.stderr.write(`\x1b[${choices.length}A`) |
| 96 | choices.forEach((c, i) => { |
| 97 | process.stderr.write(`\x1b[2K${i === selected ? '› ' : ' '}${i + 1}. ${c}\n`) |
| 98 | }) |
| 99 | } |
| 100 | |
| 101 | process.stderr.write(`${question}\n\n`) |
| 102 | |
| 103 | if (!process.stdin.isTTY || !process.stdin.setRawMode) { |
| 104 | resolve(2) |
| 105 | return |
| 106 | } |
| 107 | |
| 108 | let rendered = false |
| 109 | render() |
| 110 | rendered = true |
| 111 | process.stderr.write('\n') |
| 112 | |
| 113 | process.stdin.setRawMode(true) |
| 114 | process.stdin.resume() |
| 115 | |
| 116 | const cleanup = () => { |
| 117 | process.stdin.removeListener('data', onData) |
| 118 | process.stdin.setRawMode!(false) |
| 119 | process.stdin.pause() |
| 120 | } |
| 121 | |
| 122 | const onData = (data: Buffer) => { |
| 123 | const key = data.toString() |
| 124 | if (key === '\x03') { |
| 125 | cleanup() |
| 126 | process.exit(0) |
| 127 | } |
| 128 | // Arrow up / k |
| 129 | if (key === '\x1b[A' || key === 'k') { |
| 130 | selected = (selected - 1 + choices.length) % choices.length |
| 131 | render() |
| 132 | return |
| 133 | } |
| 134 | // Arrow down / j |
| 135 | if (key === '\x1b[B' || key === 'j') { |
| 136 | selected = (selected + 1) % choices.length |
| 137 | render() |
| 138 | return |
| 139 | } |
| 140 | // Enter — confirm current selection |
| 141 | if (key === '\r' || key === '\n') { |
| 142 | cleanup() |
| 143 | process.stderr.write('\n') |
| 144 | resolve(selected + 1) |
| 145 | return |
| 146 | } |
no test coverage detected