@hyperfrontend/questions 0.2.1 → 0.3.0
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/CHANGELOG.md +6 -0
- package/README.md +14 -5
- package/SECURITY.md +50 -15
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.cjs.js +4 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.esm.js +3 -1
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.cjs.js +2 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.esm.js +2 -1
- package/index.cjs.js +894 -278
- package/index.d.ts +16 -10
- package/index.esm.js +891 -275
- package/package.json +1 -1
package/index.cjs.js
CHANGED
|
@@ -2,14 +2,264 @@
|
|
|
2
2
|
|
|
3
3
|
const index_cjs_js = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js');
|
|
4
4
|
const node_readline = require('node:readline');
|
|
5
|
-
const
|
|
6
|
-
const index_cjs_js$2 = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/
|
|
5
|
+
const node_string_decoder = require('node:string_decoder');
|
|
6
|
+
const index_cjs_js$2 = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.cjs.js');
|
|
7
|
+
const index_cjs_js$1 = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.cjs.js');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* ANSI-aware text measurement helpers.
|
|
11
|
+
*
|
|
12
|
+
* Display width is measured in Unicode code points with ANSI escape
|
|
13
|
+
* sequences excluded. East-asian wide characters and grapheme clusters are
|
|
14
|
+
* counted as one column each; full terminal-accurate width is out of scope.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
/** Escape character introducing ANSI control sequences. */
|
|
19
|
+
const Esc = '\x1B';
|
|
20
|
+
/**
|
|
21
|
+
* Matches the ANSI escape sequence starting at `index`. The character at
|
|
22
|
+
* `index` must be the escape character (`\x1B`); callers check this before
|
|
23
|
+
* calling. Recognizes CSI (`ESC [ ... final`) and SS3 (`ESC O x`) sequences;
|
|
24
|
+
* any other escape is reported as a lone one-character escape.
|
|
25
|
+
*
|
|
26
|
+
* @param text - Text containing the sequence
|
|
27
|
+
* @param index - Position of the escape character
|
|
28
|
+
* @returns Sequence length and whether it is complete
|
|
29
|
+
*
|
|
30
|
+
* @example Matching an arrow-key sequence
|
|
31
|
+
* ```typescript
|
|
32
|
+
* matchAnsiSequence('\x1B[A', 0)
|
|
33
|
+
* // => { length: 3, complete: true }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function matchAnsiSequence(text, index) {
|
|
37
|
+
if (index + 1 >= text.length) {
|
|
38
|
+
return index_cjs_js.freeze({ length: 1, complete: false });
|
|
39
|
+
}
|
|
40
|
+
const introducer = text.charAt(index + 1);
|
|
41
|
+
if (introducer === '[') {
|
|
42
|
+
return matchCsi(text, index);
|
|
43
|
+
}
|
|
44
|
+
if (introducer === 'O') {
|
|
45
|
+
if (index + 2 >= text.length) {
|
|
46
|
+
return index_cjs_js.freeze({ length: 2, complete: false });
|
|
47
|
+
}
|
|
48
|
+
return index_cjs_js.freeze({ length: 3, complete: true });
|
|
49
|
+
}
|
|
50
|
+
return index_cjs_js.freeze({ length: 1, complete: true });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Matches a CSI sequence (`ESC [` + parameter/intermediate bytes + final
|
|
54
|
+
* byte in `0x40`-`0x7E`). A byte outside the CSI ranges terminates the match
|
|
55
|
+
* without being consumed so malformed sequences cannot swallow input.
|
|
56
|
+
*
|
|
57
|
+
* @param text - Text containing the sequence
|
|
58
|
+
* @param index - Position of the escape character
|
|
59
|
+
* @returns Sequence length and whether it is complete
|
|
60
|
+
*/
|
|
61
|
+
function matchCsi(text, index) {
|
|
62
|
+
let i = index + 2;
|
|
63
|
+
while (i < text.length) {
|
|
64
|
+
const code = text.charCodeAt(i);
|
|
65
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
66
|
+
return index_cjs_js.freeze({ length: i - index + 1, complete: true });
|
|
67
|
+
}
|
|
68
|
+
if (code < 0x20 || code > 0x3f) {
|
|
69
|
+
return index_cjs_js.freeze({ length: i - index, complete: true });
|
|
70
|
+
}
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
return index_cjs_js.freeze({ length: text.length - index, complete: false });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Removes all ANSI escape sequences from a string.
|
|
77
|
+
*
|
|
78
|
+
* @param text - Text possibly containing escape sequences
|
|
79
|
+
* @returns Text with every escape sequence removed
|
|
80
|
+
*
|
|
81
|
+
* @example Stripping color codes
|
|
82
|
+
* ```typescript
|
|
83
|
+
* stripAnsi('\x1B[36mhello\x1B[0m')
|
|
84
|
+
* // => 'hello'
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
function stripAnsi(text) {
|
|
88
|
+
let out = '';
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < text.length) {
|
|
91
|
+
if (text.charAt(i) === Esc) {
|
|
92
|
+
i += matchAnsiSequence(text, i).length;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
out += text.charAt(i);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Measures the display width of a string in terminal columns: Unicode code
|
|
102
|
+
* points with ANSI escape sequences excluded. Wide east-asian characters
|
|
103
|
+
* count as one column (documented limitation).
|
|
104
|
+
*
|
|
105
|
+
* @param text - Text to measure
|
|
106
|
+
* @returns Number of display columns
|
|
107
|
+
*
|
|
108
|
+
* @example Measuring styled text
|
|
109
|
+
* ```typescript
|
|
110
|
+
* displayWidth('\x1B[1mhi\x1B[0m')
|
|
111
|
+
* // => 2
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function displayWidth(text) {
|
|
115
|
+
return [...stripAnsi(text)].length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Incremental parser turning raw terminal input chunks into input tokens.
|
|
120
|
+
*
|
|
121
|
+
* Handles bracketed paste bodies (accumulated across chunks between
|
|
122
|
+
* `ESC[200~` and `ESC[201~`), escape-sequence keys, and printable runs.
|
|
123
|
+
* A multi-character printable run outside bracketed paste is treated as a
|
|
124
|
+
* paste from a terminal without bracketed-paste support.
|
|
10
125
|
*
|
|
11
126
|
* @internal
|
|
12
127
|
*/
|
|
128
|
+
/** Control character sent by Ctrl+C in raw mode. */
|
|
129
|
+
const CtrlC = '\x03';
|
|
130
|
+
/** Sequence a bracketed-paste-aware terminal sends before pasted content. */
|
|
131
|
+
const PasteStart = '\x1B[200~';
|
|
132
|
+
/** Sequence a bracketed-paste-aware terminal sends after pasted content. */
|
|
133
|
+
const PasteEnd = '\x1B[201~';
|
|
134
|
+
/**
|
|
135
|
+
* Kinds of tokens produced while reading terminal input.
|
|
136
|
+
*/
|
|
137
|
+
const TokenType = index_cjs_js.freeze({
|
|
138
|
+
/** A single keypress (character or escape sequence) */
|
|
139
|
+
Key: 'key',
|
|
140
|
+
/** A block of pasted text */
|
|
141
|
+
Paste: 'paste',
|
|
142
|
+
/** The output terminal was resized */
|
|
143
|
+
Resize: 'resize',
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* Builds a key token.
|
|
147
|
+
*
|
|
148
|
+
* @param value - Key character or escape sequence
|
|
149
|
+
* @returns Frozen key token
|
|
150
|
+
*/
|
|
151
|
+
function keyToken(value) {
|
|
152
|
+
return index_cjs_js.freeze({ type: TokenType.Key, value });
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Builds a paste token.
|
|
156
|
+
*
|
|
157
|
+
* @param value - Raw pasted text
|
|
158
|
+
* @returns Frozen paste token
|
|
159
|
+
*/
|
|
160
|
+
function pasteToken(value) {
|
|
161
|
+
return index_cjs_js.freeze({ type: TokenType.Paste, value });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Finds where a trailing partial occurrence of `marker` starts in `data`,
|
|
165
|
+
* scanning no earlier than `from`. Used to hold back a possible split
|
|
166
|
+
* bracketed-paste end marker until the next chunk arrives.
|
|
167
|
+
*
|
|
168
|
+
* @param data - Buffered input text
|
|
169
|
+
* @param from - First position that may be held back
|
|
170
|
+
* @param marker - Marker whose prefix may be split across chunks
|
|
171
|
+
* @returns Index where the partial marker starts, or `data.length` when the
|
|
172
|
+
* text does not end with a marker prefix
|
|
173
|
+
*/
|
|
174
|
+
function partialMarkerStart(data, from, marker) {
|
|
175
|
+
const maxLength = index_cjs_js$1.min(marker.length - 1, data.length - from);
|
|
176
|
+
for (let k = maxLength; k >= 1; k--) {
|
|
177
|
+
if (data.endsWith(marker.slice(0, k))) {
|
|
178
|
+
return data.length - k;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return data.length;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates an incremental input tokenizer.
|
|
185
|
+
*
|
|
186
|
+
* Escape sequences split across chunks are buffered until complete; a lone
|
|
187
|
+
* trailing escape is likewise buffered (prompts do not act on a bare Escape
|
|
188
|
+
* key, so delaying it until the next chunk is unobservable). Ctrl+C outside
|
|
189
|
+
* a bracketed paste is always its own key token; inside a paste body it is
|
|
190
|
+
* plain data.
|
|
191
|
+
*
|
|
192
|
+
* @returns Stateful token parser
|
|
193
|
+
*
|
|
194
|
+
* @example Parsing a paste split across two chunks
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const parser = createTokenParser()
|
|
197
|
+
* parser.feed('\x1B[200~hello ')
|
|
198
|
+
* // => []
|
|
199
|
+
* parser.feed('world\x1B[201~')
|
|
200
|
+
* // => [{ type: 'paste', value: 'hello world' }]
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
function createTokenParser() {
|
|
204
|
+
let carry = '';
|
|
205
|
+
let pasteBuffer;
|
|
206
|
+
const feed = (chunk) => {
|
|
207
|
+
const data = carry + chunk;
|
|
208
|
+
carry = '';
|
|
209
|
+
const tokens = [];
|
|
210
|
+
let pos = 0;
|
|
211
|
+
while (pos < data.length) {
|
|
212
|
+
if (pasteBuffer !== undefined) {
|
|
213
|
+
const endIndex = data.indexOf(PasteEnd, pos);
|
|
214
|
+
if (endIndex >= 0) {
|
|
215
|
+
tokens.push(pasteToken(pasteBuffer + data.slice(pos, endIndex)));
|
|
216
|
+
pasteBuffer = undefined;
|
|
217
|
+
pos = endIndex + PasteEnd.length;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const holdFrom = partialMarkerStart(data, pos, PasteEnd);
|
|
221
|
+
pasteBuffer += data.slice(pos, holdFrom);
|
|
222
|
+
carry = data.slice(holdFrom);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
const char = data.charAt(pos);
|
|
226
|
+
if (char === Esc) {
|
|
227
|
+
const match = matchAnsiSequence(data, pos);
|
|
228
|
+
if (!match.complete) {
|
|
229
|
+
carry = data.slice(pos);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const sequence = data.slice(pos, pos + match.length);
|
|
233
|
+
pos += match.length;
|
|
234
|
+
if (sequence === PasteStart) {
|
|
235
|
+
pasteBuffer = '';
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// why: a stray end marker without a matching start carries no content
|
|
239
|
+
if (sequence === PasteEnd)
|
|
240
|
+
continue;
|
|
241
|
+
tokens.push(keyToken(sequence));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (char === CtrlC) {
|
|
245
|
+
tokens.push(keyToken(char));
|
|
246
|
+
pos++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let end = pos + 1;
|
|
250
|
+
while (end < data.length && data.charAt(end) !== Esc && data.charAt(end) !== CtrlC) {
|
|
251
|
+
end++;
|
|
252
|
+
}
|
|
253
|
+
const run = data.slice(pos, end);
|
|
254
|
+
// why: raw mode delivers one keystroke per chunk, so a longer run means the terminal pasted without bracketed-paste support
|
|
255
|
+
tokens.push(run.length === 1 ? keyToken(run) : pasteToken(run));
|
|
256
|
+
pos = end;
|
|
257
|
+
}
|
|
258
|
+
return index_cjs_js.freeze(tokens);
|
|
259
|
+
};
|
|
260
|
+
return index_cjs_js.freeze({ feed });
|
|
261
|
+
}
|
|
262
|
+
|
|
13
263
|
/**
|
|
14
264
|
* Key codes for terminal navigation.
|
|
15
265
|
*/
|
|
@@ -55,6 +305,13 @@ const Ansi = index_cjs_js.freeze({
|
|
|
55
305
|
* @returns ANSI escape sequence string
|
|
56
306
|
*/
|
|
57
307
|
cursorLeft: (n) => `\x1B[${n}D`,
|
|
308
|
+
/**
|
|
309
|
+
* Generates ANSI escape code to move cursor right by specified columns.
|
|
310
|
+
*
|
|
311
|
+
* @param n - Number of columns to move right
|
|
312
|
+
* @returns ANSI escape sequence string
|
|
313
|
+
*/
|
|
314
|
+
cursorRight: (n) => `\x1B[${n}C`,
|
|
58
315
|
/** Escape code to hide cursor */
|
|
59
316
|
HideCursor: '\x1B[?25l',
|
|
60
317
|
/** Escape code to show cursor */
|
|
@@ -65,6 +322,10 @@ const Ansi = index_cjs_js.freeze({
|
|
|
65
322
|
RestoreCursor: '\x1B8',
|
|
66
323
|
/** Clear from cursor to end of screen */
|
|
67
324
|
ClearToEnd: '\x1B[J',
|
|
325
|
+
/** Ask the terminal to wrap pasted text in ESC[200~ / ESC[201~ markers */
|
|
326
|
+
BracketedPasteOn: '\x1B[?2004h',
|
|
327
|
+
/** Stop wrapping pasted text in bracketed-paste markers */
|
|
328
|
+
BracketedPasteOff: '\x1B[?2004l',
|
|
68
329
|
/** Escape code for bold text */
|
|
69
330
|
Bold: '\x1B[1m',
|
|
70
331
|
/** Escape code for dim text */
|
|
@@ -85,6 +346,12 @@ const Ansi = index_cjs_js.freeze({
|
|
|
85
346
|
/**
|
|
86
347
|
* Creates a terminal interface for interactive prompts.
|
|
87
348
|
*
|
|
349
|
+
* The first `readToken`/`readKey` call opens a read session: the input is
|
|
350
|
+
* switched to raw mode for the whole session (restored on `close`),
|
|
351
|
+
* bracketed paste mode is enabled on TTY inputs, and resize events from the
|
|
352
|
+
* output surface as resize tokens. Input chunks are tokenized by a
|
|
353
|
+
* persistent listener so no chunk is lost between reads.
|
|
354
|
+
*
|
|
88
355
|
* @param config - Terminal configuration options
|
|
89
356
|
* @returns Terminal interface with read/write methods
|
|
90
357
|
*
|
|
@@ -92,7 +359,7 @@ const Ansi = index_cjs_js.freeze({
|
|
|
92
359
|
* ```typescript
|
|
93
360
|
* const term = createTerminal()
|
|
94
361
|
* term.write('Enter name: ')
|
|
95
|
-
* const
|
|
362
|
+
* const token = await term.readToken()
|
|
96
363
|
* term.close()
|
|
97
364
|
* ```
|
|
98
365
|
*/
|
|
@@ -100,7 +367,15 @@ function createTerminal(config = {}) {
|
|
|
100
367
|
const input = config.input ?? process.stdin;
|
|
101
368
|
const output = config.output ?? process.stdout;
|
|
102
369
|
let cancelled = false;
|
|
370
|
+
let closed = false;
|
|
371
|
+
let sessionActive = false;
|
|
372
|
+
let savedRawMode = false;
|
|
103
373
|
let rl;
|
|
374
|
+
let pendingWaiter;
|
|
375
|
+
const parser = createTokenParser();
|
|
376
|
+
// why: decoding through StringDecoder keeps multibyte characters intact when a large paste is split across stream chunks mid-code-point
|
|
377
|
+
const decoder = new node_string_decoder.StringDecoder('utf8');
|
|
378
|
+
const tokenQueue = [];
|
|
104
379
|
const getReadline = () => {
|
|
105
380
|
if (!rl) {
|
|
106
381
|
rl = node_readline.createInterface({ input, output, terminal: true });
|
|
@@ -110,25 +385,78 @@ function createTerminal(config = {}) {
|
|
|
110
385
|
const write = (text) => {
|
|
111
386
|
output.write(text);
|
|
112
387
|
};
|
|
113
|
-
const
|
|
114
|
-
const
|
|
388
|
+
const deliver = (tokens) => {
|
|
389
|
+
for (const token of tokens) {
|
|
390
|
+
if (token.type === TokenType.Key && token.value === Key.CtrlC) {
|
|
391
|
+
cancelled = true;
|
|
392
|
+
}
|
|
393
|
+
// why: a resize drag fires many events; consecutive notifications collapse into one so the prompt repaints once per drained batch
|
|
394
|
+
if (token.type === TokenType.Resize && tokenQueue[tokenQueue.length - 1]?.type === TokenType.Resize) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
tokenQueue.push(token);
|
|
398
|
+
}
|
|
399
|
+
if (pendingWaiter !== undefined) {
|
|
400
|
+
const token = tokenQueue.shift();
|
|
401
|
+
if (token !== undefined) {
|
|
402
|
+
const waiter = pendingWaiter;
|
|
403
|
+
pendingWaiter = undefined;
|
|
404
|
+
waiter(token);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const onData = (data) => {
|
|
409
|
+
const text = decoder.write(data);
|
|
410
|
+
if (text !== '') {
|
|
411
|
+
deliver(parser.feed(text));
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const onResize = () => {
|
|
415
|
+
deliver(index_cjs_js.freeze([index_cjs_js.freeze({ type: TokenType.Resize })]));
|
|
416
|
+
};
|
|
417
|
+
const openSession = () => {
|
|
418
|
+
if (sessionActive)
|
|
419
|
+
return;
|
|
420
|
+
sessionActive = true;
|
|
421
|
+
savedRawMode = input.isRaw === true;
|
|
115
422
|
if (input.setRawMode) {
|
|
116
423
|
input.setRawMode(true);
|
|
424
|
+
write(Ansi.BracketedPasteOn);
|
|
117
425
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
426
|
+
input.on('data', onData);
|
|
427
|
+
output.on('resize', onResize);
|
|
428
|
+
input.resume();
|
|
429
|
+
};
|
|
430
|
+
const closeSession = () => {
|
|
431
|
+
if (!sessionActive)
|
|
432
|
+
return;
|
|
433
|
+
sessionActive = false;
|
|
434
|
+
input.removeListener('data', onData);
|
|
435
|
+
output.removeListener('resize', onResize);
|
|
436
|
+
if (input.setRawMode) {
|
|
437
|
+
write(Ansi.BracketedPasteOff);
|
|
438
|
+
input.setRawMode(savedRawMode);
|
|
439
|
+
}
|
|
440
|
+
input.pause();
|
|
441
|
+
};
|
|
442
|
+
const readToken = () => {
|
|
443
|
+
openSession();
|
|
444
|
+
const queued = tokenQueue.shift();
|
|
445
|
+
if (queued !== undefined) {
|
|
446
|
+
return index_cjs_js$2.promiseResolve(queued);
|
|
447
|
+
}
|
|
448
|
+
return index_cjs_js$2.createPromise((resolve) => {
|
|
449
|
+
pendingWaiter = resolve;
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
const readKey = async () => {
|
|
453
|
+
let token = await readToken();
|
|
454
|
+
while (token.type === TokenType.Resize) {
|
|
455
|
+
token = await readToken();
|
|
456
|
+
}
|
|
457
|
+
return token.value;
|
|
458
|
+
};
|
|
459
|
+
const readLine = () => index_cjs_js$2.createPromise((resolve) => {
|
|
132
460
|
const readline = getReadline();
|
|
133
461
|
readline.once('line', (line) => {
|
|
134
462
|
resolve(line);
|
|
@@ -148,7 +476,15 @@ function createTerminal(config = {}) {
|
|
|
148
476
|
}
|
|
149
477
|
}
|
|
150
478
|
};
|
|
479
|
+
const getSize = () => index_cjs_js.freeze({
|
|
480
|
+
columns: output.columns > 0 ? output.columns : 80,
|
|
481
|
+
rows: output.rows > 0 ? output.rows : 24,
|
|
482
|
+
});
|
|
151
483
|
const close = () => {
|
|
484
|
+
if (closed)
|
|
485
|
+
return;
|
|
486
|
+
closed = true;
|
|
487
|
+
closeSession();
|
|
152
488
|
if (rl) {
|
|
153
489
|
rl.close();
|
|
154
490
|
rl = undefined;
|
|
@@ -158,8 +494,10 @@ function createTerminal(config = {}) {
|
|
|
158
494
|
return index_cjs_js.freeze({
|
|
159
495
|
write,
|
|
160
496
|
readKey,
|
|
497
|
+
readToken,
|
|
161
498
|
readLine,
|
|
162
499
|
clearLines,
|
|
500
|
+
getSize,
|
|
163
501
|
close,
|
|
164
502
|
isCancelled: () => cancelled,
|
|
165
503
|
cancel: () => {
|
|
@@ -286,6 +624,139 @@ function renderCancelled() {
|
|
|
286
624
|
return style.dim('(cancelled)');
|
|
287
625
|
}
|
|
288
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Hard-wraps one logical line into physical rows no wider than `width`
|
|
629
|
+
* display columns. ANSI escape sequences are zero-width and never split;
|
|
630
|
+
* surrogate pairs travel together.
|
|
631
|
+
*
|
|
632
|
+
* @param line - Logical line, possibly containing ANSI sequences
|
|
633
|
+
* @param width - Maximum display columns per row (at least 1)
|
|
634
|
+
* @returns Physical rows covering the line (always at least one row)
|
|
635
|
+
*
|
|
636
|
+
* @example Wrapping a long line
|
|
637
|
+
* ```typescript
|
|
638
|
+
* wrapLine('abcdefghij', 4)
|
|
639
|
+
* // => ['abcd', 'efgh', 'ij']
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
function wrapLine(line, width) {
|
|
643
|
+
const rows = [];
|
|
644
|
+
let current = '';
|
|
645
|
+
let col = 0;
|
|
646
|
+
let i = 0;
|
|
647
|
+
while (i < line.length) {
|
|
648
|
+
if (line.charAt(i) === Esc) {
|
|
649
|
+
const { length } = matchAnsiSequence(line, i);
|
|
650
|
+
current += line.slice(i, i + length);
|
|
651
|
+
i += length;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (col === width) {
|
|
655
|
+
rows.push(current);
|
|
656
|
+
current = '';
|
|
657
|
+
col = 0;
|
|
658
|
+
}
|
|
659
|
+
const charCode = line.charCodeAt(i);
|
|
660
|
+
// why: keep surrogate pairs on the same row so code points never split
|
|
661
|
+
const charLength = charCode >= 0xd800 && charCode <= 0xdbff ? 2 : 1;
|
|
662
|
+
current += line.slice(i, i + charLength);
|
|
663
|
+
col++;
|
|
664
|
+
i += charLength;
|
|
665
|
+
}
|
|
666
|
+
rows.push(current);
|
|
667
|
+
return index_cjs_js.freeze(rows);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Creates a frame renderer bound to a terminal.
|
|
671
|
+
*
|
|
672
|
+
* @param terminal - Terminal used for size queries and output
|
|
673
|
+
* @returns Screen renderer
|
|
674
|
+
*
|
|
675
|
+
* @example Repainting a prompt line with a parked cursor
|
|
676
|
+
* ```typescript
|
|
677
|
+
* const screen = createScreen(terminal)
|
|
678
|
+
* screen.render({ lines: ['? Name: Jo'], cursor: { line: 0, col: 10 } })
|
|
679
|
+
* screen.render({ lines: ['? Name: Joe'], cursor: { line: 0, col: 11 } })
|
|
680
|
+
* ```
|
|
681
|
+
*/
|
|
682
|
+
function createScreen(terminal) {
|
|
683
|
+
let lastRows = index_cjs_js.freeze([]);
|
|
684
|
+
let lastWidth = 0;
|
|
685
|
+
let lastCursorRow = 0;
|
|
686
|
+
let lastCursorCol = 0;
|
|
687
|
+
const reflowedCursorRow = (width) => {
|
|
688
|
+
let row = 0;
|
|
689
|
+
for (const paintedRow of lastRows.slice(0, lastCursorRow)) {
|
|
690
|
+
row += index_cjs_js$1.max(1, index_cjs_js$1.ceil(displayWidth(paintedRow) / width));
|
|
691
|
+
}
|
|
692
|
+
return row + index_cjs_js$1.floor(lastCursorCol / width);
|
|
693
|
+
};
|
|
694
|
+
const eraseLastFrame = (width, viewportRows) => {
|
|
695
|
+
if (lastRows.length === 0)
|
|
696
|
+
return;
|
|
697
|
+
// why: on a width change the terminal reflowed the old paint, so the cursor's row offset is recomputed against the new width
|
|
698
|
+
const cursorRow = width === lastWidth ? lastCursorRow : reflowedCursorRow(width);
|
|
699
|
+
// why: travel is capped at the viewport height so a reflow estimate can never climb past the frame into content above it
|
|
700
|
+
const travelRows = index_cjs_js$1.min(cursorRow, viewportRows - 1);
|
|
701
|
+
let travel = Ansi.CursorStart;
|
|
702
|
+
if (travelRows > 0)
|
|
703
|
+
travel += Ansi.cursorUp(travelRows);
|
|
704
|
+
terminal.write(travel + Ansi.ClearToEnd);
|
|
705
|
+
};
|
|
706
|
+
const resolveCursor = (frame, rowsPerLine, lastRowText, totalRows, width) => {
|
|
707
|
+
if (frame.cursor === undefined) {
|
|
708
|
+
return index_cjs_js.freeze({ row: totalRows - 1, col: displayWidth(lastRowText) });
|
|
709
|
+
}
|
|
710
|
+
let row = 0;
|
|
711
|
+
for (const count of rowsPerLine.slice(0, frame.cursor.line)) {
|
|
712
|
+
row += count;
|
|
713
|
+
}
|
|
714
|
+
let extraRows = index_cjs_js$1.floor(frame.cursor.col / width);
|
|
715
|
+
let col = frame.cursor.col - extraRows * width;
|
|
716
|
+
if (col === 0 && frame.cursor.col > 0) {
|
|
717
|
+
// why: a cursor at an exact wrap boundary parks at the end of the previous physical row rather than on an unpainted row
|
|
718
|
+
extraRows -= 1;
|
|
719
|
+
col = width;
|
|
720
|
+
}
|
|
721
|
+
return index_cjs_js.freeze({ row: row + extraRows, col });
|
|
722
|
+
};
|
|
723
|
+
const render = (frame) => {
|
|
724
|
+
const size = terminal.getSize();
|
|
725
|
+
const width = index_cjs_js$1.max(1, size.columns);
|
|
726
|
+
const viewportRows = index_cjs_js$1.max(1, size.rows);
|
|
727
|
+
eraseLastFrame(width, viewportRows);
|
|
728
|
+
const wrappedRows = [];
|
|
729
|
+
const rowsPerLine = [];
|
|
730
|
+
let lastRowText = '';
|
|
731
|
+
for (const line of frame.lines) {
|
|
732
|
+
const wrapped = wrapLine(line, width);
|
|
733
|
+
rowsPerLine.push(wrapped.length);
|
|
734
|
+
for (const row of wrapped) {
|
|
735
|
+
wrappedRows.push(row);
|
|
736
|
+
lastRowText = row;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// why: a frame taller than the viewport would scroll its own top off-screen and desync the erase math, so only the tail that fits is painted
|
|
740
|
+
const dropped = index_cjs_js$1.max(0, wrappedRows.length - viewportRows);
|
|
741
|
+
const physicalRows = dropped > 0 ? wrappedRows.slice(dropped) : wrappedRows;
|
|
742
|
+
terminal.write(physicalRows.join('\n'));
|
|
743
|
+
const rawCursor = resolveCursor(frame, rowsPerLine, lastRowText, wrappedRows.length, width);
|
|
744
|
+
const cursor = rawCursor.row >= dropped ? index_cjs_js.freeze({ row: rawCursor.row - dropped, col: rawCursor.col }) : index_cjs_js.freeze({ row: 0, col: 0 });
|
|
745
|
+
let travel = Ansi.CursorStart;
|
|
746
|
+
const rowsUp = physicalRows.length - 1 - cursor.row;
|
|
747
|
+
if (rowsUp > 0)
|
|
748
|
+
travel += Ansi.cursorUp(rowsUp);
|
|
749
|
+
if (cursor.col > 0)
|
|
750
|
+
travel += Ansi.cursorRight(cursor.col);
|
|
751
|
+
terminal.write(travel);
|
|
752
|
+
lastRows = index_cjs_js.freeze(physicalRows);
|
|
753
|
+
lastWidth = width;
|
|
754
|
+
lastCursorRow = cursor.row;
|
|
755
|
+
lastCursorCol = cursor.col;
|
|
756
|
+
};
|
|
757
|
+
return index_cjs_js.freeze({ render });
|
|
758
|
+
}
|
|
759
|
+
|
|
289
760
|
/**
|
|
290
761
|
* Core types for terminal prompts.
|
|
291
762
|
*
|
|
@@ -317,11 +788,36 @@ function renderOptions(initial) {
|
|
|
317
788
|
}
|
|
318
789
|
return style.dim('(y/n)');
|
|
319
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Interprets a key or paste token as a yes/no answer. Keys accept `y`/`n`
|
|
793
|
+
* (any case); pastes accept a trimmed, lowercased `y`/`yes`/`n`/`no`.
|
|
794
|
+
*
|
|
795
|
+
* @internal
|
|
796
|
+
* @param token - Token to interpret
|
|
797
|
+
* @returns The boolean answer, or undefined when the token is not one
|
|
798
|
+
*/
|
|
799
|
+
function parseAnswer(token) {
|
|
800
|
+
const normalized = token.value.trim().toLowerCase();
|
|
801
|
+
if (token.type === TokenType.Paste) {
|
|
802
|
+
if (normalized === 'y' || normalized === 'yes')
|
|
803
|
+
return true;
|
|
804
|
+
if (normalized === 'n' || normalized === 'no')
|
|
805
|
+
return false;
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
if (normalized === 'y')
|
|
809
|
+
return true;
|
|
810
|
+
if (normalized === 'n')
|
|
811
|
+
return false;
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
320
814
|
/**
|
|
321
815
|
* Prompts for yes/no confirmation.
|
|
322
816
|
*
|
|
323
817
|
* Pure functional prompt that asks a yes/no question and returns a boolean.
|
|
324
|
-
* Supports default values and responds to y/Y/n/N keys.
|
|
818
|
+
* Supports default values and responds to y/Y/n/N keys. A pasted
|
|
819
|
+
* `y`/`yes`/`n`/`no` (trimmed, case-insensitive) is accepted; any other
|
|
820
|
+
* paste is ignored. The prompt repaints on terminal resize.
|
|
325
821
|
*
|
|
326
822
|
* @param config - Confirm prompt configuration
|
|
327
823
|
* @returns Promise resolving to boolean value or cancellation
|
|
@@ -344,40 +840,107 @@ function renderOptions(initial) {
|
|
|
344
840
|
*/
|
|
345
841
|
async function confirm(config) {
|
|
346
842
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
843
|
+
const screen = createScreen(term);
|
|
844
|
+
const finish = (value) => {
|
|
845
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(config.message) + renderSubmitted(value ? 'Yes' : 'No')]) }));
|
|
846
|
+
term.write('\n');
|
|
847
|
+
return index_cjs_js.freeze({ result: PromptResult.Submitted, value });
|
|
350
848
|
};
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
term.write(renderMessage(config.message) + renderSubmitted(value ? 'Yes' : 'No') + '\n');
|
|
849
|
+
const redraw = () => {
|
|
850
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(config.message) + renderOptions(config.initial) + ' ']) }));
|
|
354
851
|
};
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
852
|
+
try {
|
|
853
|
+
redraw();
|
|
854
|
+
while (true) {
|
|
855
|
+
const token = await term.readToken();
|
|
856
|
+
if (term.isCancelled()) {
|
|
857
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(config.message) + renderCancelled()]) }));
|
|
858
|
+
term.write('\n');
|
|
859
|
+
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
860
|
+
}
|
|
861
|
+
if (token.type === TokenType.Resize) {
|
|
862
|
+
redraw();
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const answer = parseAnswer(token);
|
|
866
|
+
if (answer !== undefined) {
|
|
867
|
+
return finish(answer);
|
|
868
|
+
}
|
|
869
|
+
if (token.value === Key.Enter && config.initial !== undefined) {
|
|
870
|
+
return finish(config.initial);
|
|
871
|
+
}
|
|
369
872
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
term.close();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Sanitizers for pasted text before it enters prompt state.
|
|
881
|
+
*
|
|
882
|
+
* @internal
|
|
883
|
+
*/
|
|
884
|
+
/**
|
|
885
|
+
* Sanitizes pasted text for insertion into a single-line input: interior
|
|
886
|
+
* runs of `\r\n`, `\r`, or `\n` collapse into a single space, leading and
|
|
887
|
+
* trailing newline runs are dropped, and all other C0 control characters
|
|
888
|
+
* (plus DEL) are removed. Pasting therefore never submits or triggers
|
|
889
|
+
* prompt actions.
|
|
890
|
+
*
|
|
891
|
+
* @param raw - Raw pasted text
|
|
892
|
+
* @returns Sanitized single-line text (possibly empty)
|
|
893
|
+
*
|
|
894
|
+
* @example Collapsing a multi-line paste
|
|
895
|
+
* ```typescript
|
|
896
|
+
* sanitizePasteText('first\r\nsecond\n')
|
|
897
|
+
* // => 'first second'
|
|
898
|
+
* ```
|
|
899
|
+
*/
|
|
900
|
+
function sanitizePasteText(raw) {
|
|
901
|
+
let out = '';
|
|
902
|
+
let pendingBreak = false;
|
|
903
|
+
for (const char of raw) {
|
|
904
|
+
if (char === '\r' || char === '\n') {
|
|
905
|
+
pendingBreak = true;
|
|
906
|
+
continue;
|
|
374
907
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
908
|
+
const code = char.charCodeAt(0);
|
|
909
|
+
if (code < 0x20 || code === 0x7f)
|
|
910
|
+
continue;
|
|
911
|
+
if (pendingBreak && out.length > 0) {
|
|
912
|
+
out += ' ';
|
|
379
913
|
}
|
|
914
|
+
pendingBreak = false;
|
|
915
|
+
out += char;
|
|
916
|
+
}
|
|
917
|
+
return out;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Extracts the first line of a paste for search-query style inputs: content
|
|
921
|
+
* is cut at the first `\r` or `\n`, then remaining C0 control characters
|
|
922
|
+
* (plus DEL) are removed.
|
|
923
|
+
*
|
|
924
|
+
* @param raw - Raw pasted text
|
|
925
|
+
* @returns Printable characters of the first pasted line
|
|
926
|
+
*
|
|
927
|
+
* @example Keeping only the first pasted line
|
|
928
|
+
* ```typescript
|
|
929
|
+
* firstPasteLine('query\nsecond line')
|
|
930
|
+
* // => 'query'
|
|
931
|
+
* ```
|
|
932
|
+
*/
|
|
933
|
+
function firstPasteLine(raw) {
|
|
934
|
+
let out = '';
|
|
935
|
+
for (const char of raw) {
|
|
936
|
+
if (char === '\r' || char === '\n')
|
|
937
|
+
break;
|
|
938
|
+
const code = char.charCodeAt(0);
|
|
939
|
+
if (code < 0x20 || code === 0x7f)
|
|
940
|
+
continue;
|
|
941
|
+
out += char;
|
|
380
942
|
}
|
|
943
|
+
return out;
|
|
381
944
|
}
|
|
382
945
|
|
|
383
946
|
/**
|
|
@@ -484,72 +1047,75 @@ function renderChoice$1(choice, isSelected, isFocused) {
|
|
|
484
1047
|
return `${pointer} ${checkbox} ${label}${hint}`;
|
|
485
1048
|
}
|
|
486
1049
|
/**
|
|
487
|
-
*
|
|
1050
|
+
* Builds the frame lines for the multiselect prompt.
|
|
488
1051
|
*
|
|
489
1052
|
* @internal
|
|
490
|
-
* @param term - Terminal interface
|
|
491
1053
|
* @param config - Prompt configuration
|
|
492
1054
|
* @param state - Current prompt state
|
|
493
|
-
* @param
|
|
494
|
-
* @returns
|
|
1055
|
+
* @param maxVisible - Maximum number of visible choices
|
|
1056
|
+
* @returns Logical lines describing the prompt
|
|
495
1057
|
*/
|
|
496
|
-
function
|
|
497
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1058
|
+
function buildLines$1(config, state, maxVisible) {
|
|
498
1059
|
const { indices: visibleIndices, startIndex } = getVisibleChoices$1(state, maxVisible);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const selectedLabels = state.selected.map((i) => state.choices[i]?.label ?? '').join(', ');
|
|
502
|
-
output += renderSubmitted(selectedLabels || 'none');
|
|
503
|
-
term.write(output);
|
|
504
|
-
return 1;
|
|
505
|
-
}
|
|
1060
|
+
const lines = [];
|
|
1061
|
+
let header = renderMessage(config.message);
|
|
506
1062
|
if (config.searchable && state.searchQuery) {
|
|
507
|
-
|
|
1063
|
+
header += style.cyan(state.searchQuery) + style.dim(' (type to filter)');
|
|
508
1064
|
}
|
|
509
1065
|
else if (config.searchable) {
|
|
510
|
-
|
|
1066
|
+
header += style.dim('(type to filter, space to toggle, enter to submit)');
|
|
511
1067
|
}
|
|
512
1068
|
else {
|
|
513
|
-
|
|
1069
|
+
header += style.dim('(space to toggle, enter to submit)');
|
|
514
1070
|
}
|
|
515
|
-
|
|
516
|
-
let lineCount = 1;
|
|
1071
|
+
lines.push(header);
|
|
517
1072
|
const minMax = [];
|
|
518
1073
|
if (config.min !== undefined)
|
|
519
1074
|
minMax.push(`min: ${config.min}`);
|
|
520
1075
|
if (config.max !== undefined)
|
|
521
1076
|
minMax.push(`max: ${config.max}`);
|
|
522
1077
|
const countHint = minMax.length > 0 ? ` (${minMax.join(', ')})` : '';
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const showScrollDown = startIndex + maxVisible < state.filteredIndices.length;
|
|
527
|
-
if (showScrollUp) {
|
|
528
|
-
term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`) + '\n');
|
|
529
|
-
lineCount++;
|
|
1078
|
+
lines.push(style.dim(` ${state.selected.length} selected${countHint}`));
|
|
1079
|
+
if (startIndex > 0) {
|
|
1080
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`));
|
|
530
1081
|
}
|
|
531
1082
|
visibleIndices.forEach((actualIndex, i) => {
|
|
532
1083
|
const choice = state.choices[actualIndex];
|
|
533
1084
|
/* istanbul ignore if -- @preserve defensive: actualIndex always valid from filteredIndices */
|
|
534
1085
|
if (!choice)
|
|
535
1086
|
return;
|
|
536
|
-
const
|
|
537
|
-
const isFocused = viewIndex === state.cursor;
|
|
1087
|
+
const isFocused = startIndex + i === state.cursor;
|
|
538
1088
|
const isSelected = arrayIncludes(state.selected, actualIndex);
|
|
539
|
-
|
|
540
|
-
term.write(Ansi.ClearLine + line + '\n');
|
|
541
|
-
lineCount++;
|
|
1089
|
+
lines.push(renderChoice$1(choice, isSelected, isFocused));
|
|
542
1090
|
});
|
|
543
|
-
if (
|
|
1091
|
+
if (startIndex + maxVisible < state.filteredIndices.length) {
|
|
544
1092
|
const remaining = state.filteredIndices.length - (startIndex + maxVisible);
|
|
545
|
-
|
|
546
|
-
lineCount++;
|
|
1093
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${remaining} more below)`));
|
|
547
1094
|
}
|
|
548
1095
|
if (state.filteredIndices.length === 0 && config.searchable) {
|
|
549
|
-
|
|
550
|
-
lineCount++;
|
|
1096
|
+
lines.push(style.dim(' No matches found'));
|
|
551
1097
|
}
|
|
552
|
-
return
|
|
1098
|
+
return index_cjs_js.freeze(lines);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Appends text to the search query, re-filtering choices.
|
|
1102
|
+
*
|
|
1103
|
+
* @internal
|
|
1104
|
+
* @param query - Text to append (a typed character or pasted line)
|
|
1105
|
+
* @param state - Current prompt state
|
|
1106
|
+
* @returns Updated state with the new query applied
|
|
1107
|
+
*/
|
|
1108
|
+
function appendSearch$1(query, state) {
|
|
1109
|
+
if (query === '')
|
|
1110
|
+
return state;
|
|
1111
|
+
const newQuery = state.searchQuery + query;
|
|
1112
|
+
return index_cjs_js.freeze({
|
|
1113
|
+
...state,
|
|
1114
|
+
searchQuery: newQuery,
|
|
1115
|
+
filteredIndices: filterChoices$1(state.choices, newQuery),
|
|
1116
|
+
cursor: 0,
|
|
1117
|
+
scrollOffset: 0,
|
|
1118
|
+
});
|
|
553
1119
|
}
|
|
554
1120
|
/**
|
|
555
1121
|
* Processes a keypress and returns updated state.
|
|
@@ -558,10 +1124,10 @@ function render$2(term, config, state, submitted) {
|
|
|
558
1124
|
* @param key - The key that was pressed
|
|
559
1125
|
* @param state - Current prompt state
|
|
560
1126
|
* @param config - Prompt configuration
|
|
1127
|
+
* @param maxVisible - Maximum number of visible choices
|
|
561
1128
|
* @returns Updated state after processing the key
|
|
562
1129
|
*/
|
|
563
|
-
function processKey$2(key, state, config) {
|
|
564
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1130
|
+
function processKey$2(key, state, config, maxVisible) {
|
|
565
1131
|
const total = state.filteredIndices.length;
|
|
566
1132
|
if (total === 0 && key !== Key.Backspace && key !== '\b')
|
|
567
1133
|
return state;
|
|
@@ -628,15 +1194,7 @@ function processKey$2(key, state, config) {
|
|
|
628
1194
|
return state;
|
|
629
1195
|
}
|
|
630
1196
|
if (key.length === 1 && key >= ' ' && key !== Key.Space) {
|
|
631
|
-
|
|
632
|
-
const newFiltered = filterChoices$1(state.choices, newQuery);
|
|
633
|
-
return index_cjs_js.freeze({
|
|
634
|
-
...state,
|
|
635
|
-
searchQuery: newQuery,
|
|
636
|
-
filteredIndices: newFiltered,
|
|
637
|
-
cursor: 0,
|
|
638
|
-
scrollOffset: 0,
|
|
639
|
-
});
|
|
1197
|
+
return appendSearch$1(key, state);
|
|
640
1198
|
}
|
|
641
1199
|
}
|
|
642
1200
|
return state;
|
|
@@ -658,11 +1216,26 @@ function validateSelection(state, config) {
|
|
|
658
1216
|
}
|
|
659
1217
|
return undefined;
|
|
660
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Resolves the visible-window size, capped so the frame fits the terminal
|
|
1221
|
+
* height (header and scroll-indicator rows reserved).
|
|
1222
|
+
*
|
|
1223
|
+
* @internal
|
|
1224
|
+
* @param term - Terminal used for size queries
|
|
1225
|
+
* @param configured - Configured maximum visible choices
|
|
1226
|
+
* @returns Effective maximum visible choices (at least 1)
|
|
1227
|
+
*/
|
|
1228
|
+
function effectiveMaxVisible$1(term, configured) {
|
|
1229
|
+
return index_cjs_js$1.max(1, index_cjs_js$1.min(configured ?? 10, term.getSize().rows - 4));
|
|
1230
|
+
}
|
|
661
1231
|
/**
|
|
662
1232
|
* Prompts for multiple selections from a list of choices.
|
|
663
1233
|
*
|
|
664
1234
|
* Pure functional prompt with arrow key navigation, space to toggle,
|
|
665
|
-
* scrolling support, min/max constraints, and optional type-to-filter
|
|
1235
|
+
* scrolling support, min/max constraints, and optional type-to-filter
|
|
1236
|
+
* search. In searchable mode, pasted text appends its first line to the
|
|
1237
|
+
* filter query; pasting never toggles or submits. The prompt repaints on
|
|
1238
|
+
* terminal resize, preserving cursor, selection, and scroll state.
|
|
666
1239
|
*
|
|
667
1240
|
* @param config - Multiselect prompt configuration
|
|
668
1241
|
* @returns Promise resolving to array of selected values or cancellation
|
|
@@ -704,55 +1277,59 @@ function validateSelection(state, config) {
|
|
|
704
1277
|
*/
|
|
705
1278
|
async function multiselect(config) {
|
|
706
1279
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1280
|
+
const screen = createScreen(term);
|
|
707
1281
|
let state = createInitialState$2(config);
|
|
708
|
-
let lineCount = 0;
|
|
709
1282
|
let errorMessage;
|
|
710
1283
|
term.write(Ansi.HideCursor);
|
|
711
|
-
const redraw = (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
term.write(Ansi.ClearToEnd);
|
|
716
|
-
lineCount = render$2(term, config, state, submitted);
|
|
717
|
-
if (errorMessage && !submitted) {
|
|
718
|
-
term.write(Ansi.ClearLine + style.yellow(` ${errorMessage}`) + '\n');
|
|
719
|
-
lineCount++;
|
|
1284
|
+
const redraw = () => {
|
|
1285
|
+
const lines = [...buildLines$1(config, state, effectiveMaxVisible$1(term, config.maxVisible))];
|
|
1286
|
+
if (errorMessage) {
|
|
1287
|
+
lines.push(style.yellow(` ${errorMessage}`));
|
|
720
1288
|
}
|
|
1289
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze(lines) }));
|
|
721
1290
|
};
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
if (
|
|
727
|
-
|
|
1291
|
+
try {
|
|
1292
|
+
redraw();
|
|
1293
|
+
while (true) {
|
|
1294
|
+
const token = await term.readToken();
|
|
1295
|
+
if (term.isCancelled()) {
|
|
1296
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(config.message) + renderCancelled()]) }));
|
|
1297
|
+
term.write('\n' + Ansi.ShowCursor);
|
|
1298
|
+
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
728
1299
|
}
|
|
729
|
-
|
|
730
|
-
term.write(renderMessage(config.message) + renderCancelled() + '\n');
|
|
731
|
-
term.write(Ansi.ShowCursor);
|
|
732
|
-
term.close();
|
|
733
|
-
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
734
|
-
}
|
|
735
|
-
if (key === Key.Enter) {
|
|
736
|
-
const validationError = validateSelection(state, config);
|
|
737
|
-
if (validationError) {
|
|
738
|
-
errorMessage = validationError;
|
|
1300
|
+
if (token.type === TokenType.Resize) {
|
|
739
1301
|
redraw();
|
|
740
1302
|
continue;
|
|
741
1303
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1304
|
+
if (token.type === TokenType.Paste) {
|
|
1305
|
+
if (config.searchable) {
|
|
1306
|
+
errorMessage = undefined;
|
|
1307
|
+
state = appendSearch$1(firstPasteLine(token.value), state);
|
|
1308
|
+
redraw();
|
|
1309
|
+
}
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (token.value === Key.Enter) {
|
|
1313
|
+
const validationError = validateSelection(state, config);
|
|
1314
|
+
if (validationError) {
|
|
1315
|
+
errorMessage = validationError;
|
|
1316
|
+
redraw();
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const selectedValues = state.selected.map((i) => state.choices[i]?.value).filter((v) => v !== undefined);
|
|
1320
|
+
const selectedLabels = state.selected.map((i) => state.choices[i]?.label ?? '').join(', ');
|
|
1321
|
+
const submittedLine = renderMessage(config.message) + renderSubmitted(selectedLabels || 'none');
|
|
1322
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([submittedLine]) }));
|
|
1323
|
+
term.write('\n' + Ansi.ShowCursor);
|
|
1324
|
+
return index_cjs_js.freeze({ result: PromptResult.Submitted, value: index_cjs_js.freeze(selectedValues) });
|
|
745
1325
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
term.write(Ansi.ShowCursor);
|
|
750
|
-
term.close();
|
|
751
|
-
return index_cjs_js.freeze({ result: PromptResult.Submitted, value: index_cjs_js.freeze(selectedValues) });
|
|
1326
|
+
errorMessage = undefined;
|
|
1327
|
+
state = processKey$2(token.value, state, config, effectiveMaxVisible$1(term, config.maxVisible));
|
|
1328
|
+
redraw();
|
|
752
1329
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1330
|
+
}
|
|
1331
|
+
finally {
|
|
1332
|
+
term.close();
|
|
756
1333
|
}
|
|
757
1334
|
}
|
|
758
1335
|
|
|
@@ -839,65 +1416,66 @@ function renderChoice(choice, isFocused) {
|
|
|
839
1416
|
return `${pointer} ${label}${hint}`;
|
|
840
1417
|
}
|
|
841
1418
|
/**
|
|
842
|
-
*
|
|
1419
|
+
* Builds the frame lines for the select prompt.
|
|
843
1420
|
*
|
|
844
1421
|
* @internal
|
|
845
|
-
* @param term - Terminal interface
|
|
846
1422
|
* @param config - Prompt configuration
|
|
847
1423
|
* @param state - Current prompt state
|
|
848
|
-
* @param
|
|
849
|
-
* @returns
|
|
1424
|
+
* @param maxVisible - Maximum number of visible choices
|
|
1425
|
+
* @returns Logical lines describing the prompt
|
|
850
1426
|
*/
|
|
851
|
-
function
|
|
852
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1427
|
+
function buildLines(config, state, maxVisible) {
|
|
853
1428
|
const { indices: visibleIndices, startIndex } = getVisibleChoices(state, maxVisible);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const actualIndex = state.filteredIndices[state.cursor];
|
|
857
|
-
const selectedChoice = actualIndex !== undefined ? state.choices[actualIndex] : undefined;
|
|
858
|
-
/* istanbul ignore next -- @preserve defensive: cursor always within bounds */
|
|
859
|
-
output += renderSubmitted(selectedChoice?.label ?? '');
|
|
860
|
-
term.write(output);
|
|
861
|
-
return 1;
|
|
862
|
-
}
|
|
1429
|
+
const lines = [];
|
|
1430
|
+
let header = renderMessage(config.message);
|
|
863
1431
|
if (config.searchable && state.searchQuery) {
|
|
864
|
-
|
|
1432
|
+
header += style.cyan(state.searchQuery) + style.dim(' (type to filter)');
|
|
865
1433
|
}
|
|
866
1434
|
else if (config.searchable) {
|
|
867
|
-
|
|
1435
|
+
header += style.dim('(type to filter, enter to select)');
|
|
868
1436
|
}
|
|
869
1437
|
else {
|
|
870
|
-
|
|
1438
|
+
header += style.dim('(use arrows, enter to select)');
|
|
871
1439
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const showScrollDown = startIndex + maxVisible < state.filteredIndices.length;
|
|
876
|
-
if (showScrollUp) {
|
|
877
|
-
term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`) + '\n');
|
|
878
|
-
lineCount++;
|
|
1440
|
+
lines.push(header);
|
|
1441
|
+
if (startIndex > 0) {
|
|
1442
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`));
|
|
879
1443
|
}
|
|
880
1444
|
visibleIndices.forEach((actualIndex, i) => {
|
|
881
1445
|
const choice = state.choices[actualIndex];
|
|
882
1446
|
/* istanbul ignore if -- @preserve defensive: actualIndex always valid from filteredIndices */
|
|
883
1447
|
if (!choice)
|
|
884
1448
|
return;
|
|
885
|
-
|
|
886
|
-
const isFocused = viewIndex === state.cursor;
|
|
887
|
-
const line = renderChoice(choice, isFocused);
|
|
888
|
-
term.write(Ansi.ClearLine + line + '\n');
|
|
889
|
-
lineCount++;
|
|
1449
|
+
lines.push(renderChoice(choice, startIndex + i === state.cursor));
|
|
890
1450
|
});
|
|
891
|
-
if (
|
|
1451
|
+
if (startIndex + maxVisible < state.filteredIndices.length) {
|
|
892
1452
|
const remaining = state.filteredIndices.length - (startIndex + maxVisible);
|
|
893
|
-
|
|
894
|
-
lineCount++;
|
|
1453
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${remaining} more below)`));
|
|
895
1454
|
}
|
|
896
1455
|
if (state.filteredIndices.length === 0 && config.searchable) {
|
|
897
|
-
|
|
898
|
-
lineCount++;
|
|
1456
|
+
lines.push(style.dim(' No matches found'));
|
|
899
1457
|
}
|
|
900
|
-
return
|
|
1458
|
+
return index_cjs_js.freeze(lines);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Appends text to the search query, re-filtering choices.
|
|
1462
|
+
*
|
|
1463
|
+
* @internal
|
|
1464
|
+
* @param query - Text to append (a typed character or pasted line)
|
|
1465
|
+
* @param state - Current prompt state
|
|
1466
|
+
* @returns Updated state with the new query applied
|
|
1467
|
+
*/
|
|
1468
|
+
function appendSearch(query, state) {
|
|
1469
|
+
if (query === '')
|
|
1470
|
+
return state;
|
|
1471
|
+
const newQuery = state.searchQuery + query;
|
|
1472
|
+
return index_cjs_js.freeze({
|
|
1473
|
+
...state,
|
|
1474
|
+
searchQuery: newQuery,
|
|
1475
|
+
filteredIndices: filterChoices(state.choices, newQuery),
|
|
1476
|
+
cursor: 0,
|
|
1477
|
+
scrollOffset: 0,
|
|
1478
|
+
});
|
|
901
1479
|
}
|
|
902
1480
|
/**
|
|
903
1481
|
* Processes a keypress and returns updated state.
|
|
@@ -906,10 +1484,10 @@ function render$1(term, config, state, submitted) {
|
|
|
906
1484
|
* @param key - The key that was pressed
|
|
907
1485
|
* @param state - Current prompt state
|
|
908
1486
|
* @param config - Prompt configuration
|
|
1487
|
+
* @param maxVisible - Maximum number of visible choices
|
|
909
1488
|
* @returns Updated state after processing the key
|
|
910
1489
|
*/
|
|
911
|
-
function processKey$1(key, state, config) {
|
|
912
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1490
|
+
function processKey$1(key, state, config, maxVisible) {
|
|
913
1491
|
const total = state.filteredIndices.length;
|
|
914
1492
|
if (total === 0 && key !== Key.Backspace && key !== '\b')
|
|
915
1493
|
return state;
|
|
@@ -955,15 +1533,7 @@ function processKey$1(key, state, config) {
|
|
|
955
1533
|
return state;
|
|
956
1534
|
}
|
|
957
1535
|
if (key.length === 1 && key >= ' ') {
|
|
958
|
-
|
|
959
|
-
const newFiltered = filterChoices(state.choices, newQuery);
|
|
960
|
-
return index_cjs_js.freeze({
|
|
961
|
-
...state,
|
|
962
|
-
searchQuery: newQuery,
|
|
963
|
-
filteredIndices: newFiltered,
|
|
964
|
-
cursor: 0,
|
|
965
|
-
scrollOffset: 0,
|
|
966
|
-
});
|
|
1536
|
+
return appendSearch(key, state);
|
|
967
1537
|
}
|
|
968
1538
|
}
|
|
969
1539
|
return state;
|
|
@@ -972,7 +1542,10 @@ function processKey$1(key, state, config) {
|
|
|
972
1542
|
* Prompts for single selection from a list of choices.
|
|
973
1543
|
*
|
|
974
1544
|
* Pure functional prompt with arrow key navigation, scrolling support,
|
|
975
|
-
* optional disabled choices, and optional type-to-filter search.
|
|
1545
|
+
* optional disabled choices, and optional type-to-filter search. In
|
|
1546
|
+
* searchable mode, pasted text appends its first line to the filter query.
|
|
1547
|
+
* The prompt repaints on terminal resize, preserving cursor and scroll
|
|
1548
|
+
* state.
|
|
976
1549
|
*
|
|
977
1550
|
* @param config - Select prompt configuration
|
|
978
1551
|
* @returns Promise resolving to selected value or cancellation
|
|
@@ -1016,52 +1589,66 @@ function processKey$1(key, state, config) {
|
|
|
1016
1589
|
*/
|
|
1017
1590
|
async function select(config) {
|
|
1018
1591
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1592
|
+
const screen = createScreen(term);
|
|
1019
1593
|
let state = createInitialState$1(config);
|
|
1020
|
-
let lineCount = 0;
|
|
1021
1594
|
term.write(Ansi.HideCursor);
|
|
1022
|
-
const redraw = (
|
|
1023
|
-
|
|
1024
|
-
term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
|
|
1025
|
-
}
|
|
1026
|
-
term.write(Ansi.ClearToEnd);
|
|
1027
|
-
lineCount = render$1(term, config, state, submitted);
|
|
1595
|
+
const redraw = () => {
|
|
1596
|
+
screen.render(index_cjs_js.freeze({ lines: buildLines(config, state, effectiveMaxVisible(term, config.maxVisible)) }));
|
|
1028
1597
|
};
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
if (
|
|
1034
|
-
|
|
1598
|
+
try {
|
|
1599
|
+
redraw();
|
|
1600
|
+
while (true) {
|
|
1601
|
+
const token = await term.readToken();
|
|
1602
|
+
if (term.isCancelled()) {
|
|
1603
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(config.message) + renderCancelled()]) }));
|
|
1604
|
+
term.write('\n' + Ansi.ShowCursor);
|
|
1605
|
+
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
1035
1606
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
term.write(Ansi.ShowCursor);
|
|
1039
|
-
term.close();
|
|
1040
|
-
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
1041
|
-
}
|
|
1042
|
-
if (key === Key.Enter) {
|
|
1043
|
-
const actualIndex = state.filteredIndices[state.cursor];
|
|
1044
|
-
if (actualIndex === undefined) {
|
|
1607
|
+
if (token.type === TokenType.Resize) {
|
|
1608
|
+
redraw();
|
|
1045
1609
|
continue;
|
|
1046
1610
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1611
|
+
if (token.type === TokenType.Paste) {
|
|
1612
|
+
if (config.searchable) {
|
|
1613
|
+
state = appendSearch(firstPasteLine(token.value), state);
|
|
1614
|
+
redraw();
|
|
1615
|
+
}
|
|
1049
1616
|
continue;
|
|
1050
1617
|
}
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1618
|
+
if (token.value === Key.Enter) {
|
|
1619
|
+
const actualIndex = state.filteredIndices[state.cursor];
|
|
1620
|
+
if (actualIndex === undefined) {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const selectedChoice = state.choices[actualIndex];
|
|
1624
|
+
if (!selectedChoice || selectedChoice.disabled) {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const submittedLine = renderMessage(config.message) + renderSubmitted(selectedChoice.label);
|
|
1628
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([submittedLine]) }));
|
|
1629
|
+
term.write('\n' + Ansi.ShowCursor);
|
|
1630
|
+
return index_cjs_js.freeze({ result: PromptResult.Submitted, value: selectedChoice.value });
|
|
1053
1631
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
term.write('\n');
|
|
1057
|
-
term.write(Ansi.ShowCursor);
|
|
1058
|
-
term.close();
|
|
1059
|
-
return index_cjs_js.freeze({ result: PromptResult.Submitted, value: selectedChoice.value });
|
|
1632
|
+
state = processKey$1(token.value, state, config, effectiveMaxVisible(term, config.maxVisible));
|
|
1633
|
+
redraw();
|
|
1060
1634
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1635
|
+
}
|
|
1636
|
+
finally {
|
|
1637
|
+
term.close();
|
|
1063
1638
|
}
|
|
1064
1639
|
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Resolves the visible-window size, capped so the frame fits the terminal
|
|
1642
|
+
* height (header and scroll-indicator rows reserved).
|
|
1643
|
+
*
|
|
1644
|
+
* @internal
|
|
1645
|
+
* @param term - Terminal used for size queries
|
|
1646
|
+
* @param configured - Configured maximum visible choices
|
|
1647
|
+
* @returns Effective maximum visible choices (at least 1)
|
|
1648
|
+
*/
|
|
1649
|
+
function effectiveMaxVisible(term, configured) {
|
|
1650
|
+
return index_cjs_js$1.max(1, index_cjs_js$1.min(configured ?? 10, term.getSize().rows - 4));
|
|
1651
|
+
}
|
|
1065
1652
|
|
|
1066
1653
|
/**
|
|
1067
1654
|
* Creates initial state for text prompt.
|
|
@@ -1078,40 +1665,63 @@ function createInitialState(config) {
|
|
|
1078
1665
|
};
|
|
1079
1666
|
}
|
|
1080
1667
|
/**
|
|
1081
|
-
*
|
|
1668
|
+
* Builds the current prompt message text.
|
|
1669
|
+
*
|
|
1670
|
+
* @internal
|
|
1671
|
+
* @param config - Prompt configuration
|
|
1672
|
+
* @param value - Current input value
|
|
1673
|
+
* @returns Message text for the current value
|
|
1674
|
+
*/
|
|
1675
|
+
function messageText(config, value) {
|
|
1676
|
+
return config.renderMessage ? config.renderMessage(value) : config.message;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Builds the frame for the text prompt.
|
|
1082
1680
|
*
|
|
1083
1681
|
* @internal
|
|
1084
|
-
* @param term - Terminal interface for output
|
|
1085
1682
|
* @param config - Prompt configuration
|
|
1086
1683
|
* @param state - Current prompt state
|
|
1087
1684
|
* @param submitted - Whether the prompt has been submitted
|
|
1088
|
-
* @returns
|
|
1685
|
+
* @returns Frame describing lines and cursor position
|
|
1089
1686
|
*/
|
|
1090
|
-
function
|
|
1687
|
+
function buildFrame(config, state, submitted) {
|
|
1091
1688
|
const displayValue = config.format ? config.format(state.value) : state.value;
|
|
1092
|
-
|
|
1093
|
-
let output = renderMessage(messageText);
|
|
1094
|
-
let trailingChars = 0;
|
|
1689
|
+
let line = renderMessage(messageText(config, state.value));
|
|
1095
1690
|
if (submitted) {
|
|
1096
|
-
|
|
1691
|
+
line += renderSubmitted(displayValue || config.initial || '');
|
|
1692
|
+
return index_cjs_js.freeze({ lines: index_cjs_js.freeze([line]) });
|
|
1097
1693
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
trailingChars += config.initial.length;
|
|
1104
|
-
}
|
|
1694
|
+
line += displayValue;
|
|
1695
|
+
let trailingWidth = displayWidth(state.value.slice(state.cursorPos));
|
|
1696
|
+
if (config.initial && !state.value) {
|
|
1697
|
+
line += style.dim(config.initial);
|
|
1698
|
+
trailingWidth += displayWidth(config.initial);
|
|
1105
1699
|
}
|
|
1106
|
-
|
|
1700
|
+
const cursor = index_cjs_js.freeze({ line: 0, col: displayWidth(line) - trailingWidth });
|
|
1107
1701
|
if (state.error) {
|
|
1108
|
-
|
|
1109
|
-
return 2;
|
|
1110
|
-
}
|
|
1111
|
-
if (!submitted && trailingChars > 0) {
|
|
1112
|
-
term.write(Ansi.cursorLeft(trailingChars));
|
|
1702
|
+
return index_cjs_js.freeze({ lines: index_cjs_js.freeze([line, style.yellow(` ${state.error}`)]), cursor });
|
|
1113
1703
|
}
|
|
1114
|
-
return
|
|
1704
|
+
return index_cjs_js.freeze({ lines: index_cjs_js.freeze([line]), cursor });
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Inserts text at the cursor position, moving the cursor past it.
|
|
1708
|
+
*
|
|
1709
|
+
* @internal
|
|
1710
|
+
* @param insert - Text to insert (already sanitized for pastes)
|
|
1711
|
+
* @param state - Current prompt state
|
|
1712
|
+
* @returns Updated state after insertion
|
|
1713
|
+
*/
|
|
1714
|
+
function insertText(insert, state) {
|
|
1715
|
+
if (insert === '')
|
|
1716
|
+
return state;
|
|
1717
|
+
const before = state.value.slice(0, state.cursorPos);
|
|
1718
|
+
const after = state.value.slice(state.cursorPos);
|
|
1719
|
+
return {
|
|
1720
|
+
...state,
|
|
1721
|
+
value: before + insert + after,
|
|
1722
|
+
cursorPos: state.cursorPos + insert.length,
|
|
1723
|
+
error: undefined,
|
|
1724
|
+
};
|
|
1115
1725
|
}
|
|
1116
1726
|
/**
|
|
1117
1727
|
* Processes a keypress and returns updated state.
|
|
@@ -1123,14 +1733,7 @@ function render(term, config, state, submitted) {
|
|
|
1123
1733
|
*/
|
|
1124
1734
|
function processKey(key, state) {
|
|
1125
1735
|
if (key.length === 1 && key >= ' ' && key !== Key.Backspace) {
|
|
1126
|
-
|
|
1127
|
-
const after = state.value.slice(state.cursorPos);
|
|
1128
|
-
return {
|
|
1129
|
-
...state,
|
|
1130
|
-
value: before + key + after,
|
|
1131
|
-
cursorPos: state.cursorPos + 1,
|
|
1132
|
-
error: undefined,
|
|
1133
|
-
};
|
|
1736
|
+
return insertText(key, state);
|
|
1134
1737
|
}
|
|
1135
1738
|
if (key === Key.Backspace || key === '\b') {
|
|
1136
1739
|
if (state.cursorPos > 0) {
|
|
@@ -1147,13 +1750,13 @@ function processKey(key, state) {
|
|
|
1147
1750
|
if (key === Key.Left) {
|
|
1148
1751
|
return {
|
|
1149
1752
|
...state,
|
|
1150
|
-
cursorPos: index_cjs_js$
|
|
1753
|
+
cursorPos: index_cjs_js$1.max(0, state.cursorPos - 1),
|
|
1151
1754
|
};
|
|
1152
1755
|
}
|
|
1153
1756
|
if (key === Key.Right) {
|
|
1154
1757
|
return {
|
|
1155
1758
|
...state,
|
|
1156
|
-
cursorPos: index_cjs_js$
|
|
1759
|
+
cursorPos: index_cjs_js$1.min(state.value.length, state.cursorPos + 1),
|
|
1157
1760
|
};
|
|
1158
1761
|
}
|
|
1159
1762
|
return state;
|
|
@@ -1162,7 +1765,11 @@ function processKey(key, state) {
|
|
|
1162
1765
|
* Prompts for text input with optional validation.
|
|
1163
1766
|
*
|
|
1164
1767
|
* Pure functional prompt that reads text from the user with support for
|
|
1165
|
-
* default values, input validation, and display formatting.
|
|
1768
|
+
* default values, input validation, and display formatting. Pasted text is
|
|
1769
|
+
* sanitized (newlines collapse to spaces, control characters are removed)
|
|
1770
|
+
* and inserted at the cursor without ever auto-submitting. The prompt
|
|
1771
|
+
* repaints on terminal resize, preserving value, cursor, and any
|
|
1772
|
+
* validation error.
|
|
1166
1773
|
*
|
|
1167
1774
|
* @param config - Text prompt configuration
|
|
1168
1775
|
* @returns Promise resolving to submitted value or cancellation
|
|
@@ -1196,40 +1803,49 @@ function processKey(key, state) {
|
|
|
1196
1803
|
*/
|
|
1197
1804
|
async function text(config) {
|
|
1198
1805
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1806
|
+
const screen = createScreen(term);
|
|
1199
1807
|
let state = createInitialState(config);
|
|
1200
|
-
let lineCount = 0;
|
|
1201
1808
|
const redraw = (submitted = false) => {
|
|
1202
|
-
|
|
1203
|
-
term.clearLines(lineCount);
|
|
1204
|
-
}
|
|
1205
|
-
lineCount = render(term, config, state, submitted);
|
|
1809
|
+
screen.render(buildFrame(config, state, submitted));
|
|
1206
1810
|
};
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1811
|
+
try {
|
|
1812
|
+
redraw();
|
|
1813
|
+
while (true) {
|
|
1814
|
+
const token = await term.readToken();
|
|
1815
|
+
if (term.isCancelled()) {
|
|
1816
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze([renderMessage(messageText(config, state.value)) + renderCancelled()]) }));
|
|
1817
|
+
term.write('\n');
|
|
1818
|
+
return index_cjs_js.freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
1819
|
+
}
|
|
1820
|
+
if (token.type === TokenType.Resize) {
|
|
1821
|
+
redraw();
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (token.type === TokenType.Paste) {
|
|
1825
|
+
state = insertText(sanitizePasteText(token.value), state);
|
|
1826
|
+
redraw();
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (token.value === Key.Enter) {
|
|
1830
|
+
const value = state.value || config.initial || '';
|
|
1831
|
+
if (config.validate) {
|
|
1832
|
+
const errorMessage = config.validate(value);
|
|
1833
|
+
if (errorMessage) {
|
|
1834
|
+
state = { ...state, error: errorMessage };
|
|
1835
|
+
redraw();
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1224
1838
|
}
|
|
1839
|
+
redraw(true);
|
|
1840
|
+
term.write('\n');
|
|
1841
|
+
return index_cjs_js.freeze({ result: PromptResult.Submitted, value });
|
|
1225
1842
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
term.close();
|
|
1229
|
-
return index_cjs_js.freeze({ result: PromptResult.Submitted, value });
|
|
1843
|
+
state = processKey(token.value, state);
|
|
1844
|
+
redraw();
|
|
1230
1845
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1846
|
+
}
|
|
1847
|
+
finally {
|
|
1848
|
+
term.close();
|
|
1233
1849
|
}
|
|
1234
1850
|
}
|
|
1235
1851
|
|