create-bakery 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/prompt.ts +82 -48
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/prompt.ts
CHANGED
|
@@ -138,31 +138,70 @@ function clearLines(n: number): void {
|
|
|
138
138
|
write(`${ESC}[0J`)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
yield text[i]!
|
|
159
|
-
i += 1
|
|
160
|
-
}
|
|
141
|
+
/**
|
|
142
|
+
* Split what one read of stdin delivered into keypresses.
|
|
143
|
+
*
|
|
144
|
+
* A single read can carry a whole escape sequence, and on a fast paste it can
|
|
145
|
+
* carry several keys at once. Arrow keys are the only multi-byte sequence
|
|
146
|
+
* handled, so they are matched first and the rest is split by character:
|
|
147
|
+
* by code point, so a character outside the basic plane stays one key.
|
|
148
|
+
*/
|
|
149
|
+
export function splitKeys(text: string): string[] {
|
|
150
|
+
const keys: string[] = []
|
|
151
|
+
let i = 0
|
|
152
|
+
while (i < text.length) {
|
|
153
|
+
if (text.startsWith(`${ESC}[`, i) && i + 2 < text.length) {
|
|
154
|
+
keys.push(text.slice(i, i + 3))
|
|
155
|
+
i += 3
|
|
156
|
+
continue
|
|
161
157
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
const key = String.fromCodePoint(text.codePointAt(i)!)
|
|
159
|
+
keys.push(key)
|
|
160
|
+
i += key.length
|
|
165
161
|
}
|
|
162
|
+
return keys
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Hand keypresses to `onKey` until it returns true, then let go of stdin
|
|
167
|
+
* without closing it, so the next prompt can read from it too.
|
|
168
|
+
*
|
|
169
|
+
* Listeners, not `for await (const chunk of process.stdin)`. Leaving a
|
|
170
|
+
* `for await` early returns the stream's iterator, and returning a readable's
|
|
171
|
+
* iterator destroys the stream: in 2.1.1 the first prompt answered and the
|
|
172
|
+
* second died on the destroyed stdin with `AbortError: The operation was
|
|
173
|
+
* aborted`, on any terminal, whenever more than one question was asked.
|
|
174
|
+
*
|
|
175
|
+
* Keys after the answering one, in the same read, are dropped: they were
|
|
176
|
+
* typed before the next prompt was on screen.
|
|
177
|
+
*/
|
|
178
|
+
function readKeys(onKey: (key: string) => boolean): Promise<void> {
|
|
179
|
+
const stdin = process.stdin
|
|
180
|
+
return new Promise(resolve => {
|
|
181
|
+
const finish = () => {
|
|
182
|
+
stdin.off('data', onData)
|
|
183
|
+
stdin.off('end', onEnd)
|
|
184
|
+
stdin.setRawMode?.(false)
|
|
185
|
+
stdin.pause()
|
|
186
|
+
resolve()
|
|
187
|
+
}
|
|
188
|
+
const onData = (chunk: Buffer | string) => {
|
|
189
|
+
for (const key of splitKeys(Buffer.from(chunk).toString('utf8'))) {
|
|
190
|
+
if (onKey(key)) return finish()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// The other end went away. Taken as Ctrl-D, which every prompt treats as
|
|
194
|
+
// a cancel, rather than waiting for a key that cannot come.
|
|
195
|
+
const onEnd = () => {
|
|
196
|
+
onKey(KEY.ctrlD)
|
|
197
|
+
finish()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
stdin.setRawMode?.(true)
|
|
201
|
+
stdin.on('data', onData)
|
|
202
|
+
stdin.on('end', onEnd)
|
|
203
|
+
stdin.resume()
|
|
204
|
+
})
|
|
166
205
|
}
|
|
167
206
|
|
|
168
207
|
export async function confirm(
|
|
@@ -172,17 +211,19 @@ export async function confirm(
|
|
|
172
211
|
const suffix = fallback ? 'Y/n' : 'y/N'
|
|
173
212
|
write(`${GREEN}?${RESET} ${question} ${DIM}(${suffix})${RESET} `)
|
|
174
213
|
|
|
175
|
-
|
|
214
|
+
let answer: boolean | null = null
|
|
215
|
+
await readKeys(key => {
|
|
176
216
|
const result = applyConfirmKey(key, fallback)
|
|
177
|
-
if (result.kind === 'ignore')
|
|
217
|
+
if (result.kind === 'ignore') return false
|
|
178
218
|
if (result.kind === 'cancel') {
|
|
179
219
|
write('\n')
|
|
180
|
-
return
|
|
220
|
+
return true
|
|
181
221
|
}
|
|
222
|
+
answer = result.value
|
|
182
223
|
write(`${CYAN}${result.value ? 'yes' : 'no'}${RESET}\n`)
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
return
|
|
224
|
+
return true
|
|
225
|
+
})
|
|
226
|
+
return answer
|
|
186
227
|
}
|
|
187
228
|
|
|
188
229
|
/** Returns the chosen ids, or `null` if the user canceled. */
|
|
@@ -191,41 +232,34 @@ export async function multiselect(
|
|
|
191
232
|
choices: Choice[],
|
|
192
233
|
): Promise<string[] | null> {
|
|
193
234
|
let state: MultiselectState = { choices, cursor: 0, selected: new Set() }
|
|
194
|
-
|
|
195
235
|
let frame = renderMultiselect(question, state)
|
|
196
236
|
write(`${ESC}[?25l${frame}`) // hide the cursor while redrawing
|
|
197
237
|
|
|
238
|
+
let chosen: string[] | null = null
|
|
198
239
|
try {
|
|
199
|
-
|
|
240
|
+
await readKeys(key => {
|
|
200
241
|
const result = applyKey(state, key)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return null
|
|
204
|
-
}
|
|
242
|
+
clearLines(frame.split('\n').length - 1)
|
|
243
|
+
if (result.kind === 'cancel') return true
|
|
205
244
|
|
|
206
|
-
const previous = frame
|
|
207
245
|
state = result.state
|
|
208
|
-
frame = renderMultiselect(question, state)
|
|
209
|
-
|
|
210
246
|
if (result.kind === 'submit') {
|
|
211
|
-
|
|
212
|
-
const names =
|
|
213
|
-
.filter(c => state.selected.has(c.id))
|
|
214
|
-
.map(c => c.label)
|
|
247
|
+
const picked = state.choices.filter(c => state.selected.has(c.id))
|
|
248
|
+
const names = picked.map(c => c.label)
|
|
215
249
|
write(
|
|
216
250
|
`${GREEN}?${RESET} ${question} ` +
|
|
217
251
|
`${CYAN}${names.length ? names.join(', ') : 'none'}${RESET}\n`,
|
|
218
252
|
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.map(c => c.id)
|
|
253
|
+
chosen = picked.map(c => c.id)
|
|
254
|
+
return true
|
|
222
255
|
}
|
|
223
256
|
|
|
224
|
-
|
|
257
|
+
frame = renderMultiselect(question, state)
|
|
225
258
|
write(frame)
|
|
226
|
-
|
|
259
|
+
return false
|
|
260
|
+
})
|
|
227
261
|
} finally {
|
|
228
262
|
write(`${ESC}[?25h`) // always give the cursor back
|
|
229
263
|
}
|
|
230
|
-
return
|
|
264
|
+
return chosen
|
|
231
265
|
}
|