@hyperfrontend/questions 0.2.0 → 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 +13 -1
- package/README.md +14 -5
- package/SECURITY.md +50 -15
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.cjs.js +11 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/math/index.esm.js +7 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js +5 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.esm.js +4 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.cjs.js +8 -0
- package/_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/promise/index.esm.js +6 -0
- package/index.cjs.js +888 -350
- package/index.d.ts +339 -12
- package/index.esm.js +866 -328
- package/package.json +13 -3
- package/index.cjs.js.map +0 -1
- package/index.esm.js.map +0 -1
- package/render.d.ts +0 -120
- package/render.d.ts.map +0 -1
- package/terminal.d.ts +0 -118
- package/terminal.d.ts.map +0 -1
- package/types.d.ts +0 -112
- package/types.d.ts.map +0 -1
package/index.cjs.js
CHANGED
|
@@ -1,78 +1,269 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const index_cjs_js = require('./_dependencies/@hyperfrontend/immutable-api-utils/built-in-copy/object/index.cjs.js');
|
|
4
|
+
const node_readline = require('node:readline');
|
|
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');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
10
|
+
* ANSI-aware text measurement helpers.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
15
|
*
|
|
11
|
-
* @
|
|
16
|
+
* @internal
|
|
12
17
|
*/
|
|
13
|
-
|
|
18
|
+
/** Escape character introducing ANSI control sequences. */
|
|
19
|
+
const Esc = '\x1B';
|
|
14
20
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
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
|
|
22
29
|
*
|
|
23
|
-
* @
|
|
30
|
+
* @example Matching an arrow-key sequence
|
|
31
|
+
* ```typescript
|
|
32
|
+
* matchAnsiSequence('\x1B[A', 0)
|
|
33
|
+
* // => { length: 3, complete: true }
|
|
34
|
+
* ```
|
|
24
35
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|
|
28
52
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
31
56
|
*
|
|
32
|
-
* @param
|
|
33
|
-
* @
|
|
57
|
+
* @param text - Text containing the sequence
|
|
58
|
+
* @param index - Position of the escape character
|
|
59
|
+
* @returns Sequence length and whether it is complete
|
|
34
60
|
*/
|
|
35
|
-
|
|
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
|
+
}
|
|
36
75
|
/**
|
|
37
|
-
*
|
|
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
|
+
* ```
|
|
38
86
|
*/
|
|
39
|
-
|
|
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
|
+
}
|
|
40
100
|
/**
|
|
41
|
-
*
|
|
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
|
+
* ```
|
|
42
113
|
*/
|
|
43
|
-
|
|
114
|
+
function displayWidth(text) {
|
|
115
|
+
return [...stripAnsi(text)].length;
|
|
116
|
+
}
|
|
117
|
+
|
|
44
118
|
/**
|
|
45
|
-
*
|
|
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.
|
|
125
|
+
*
|
|
126
|
+
* @internal
|
|
46
127
|
*/
|
|
47
|
-
|
|
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~';
|
|
48
134
|
/**
|
|
49
|
-
*
|
|
135
|
+
* Kinds of tokens produced while reading terminal input.
|
|
50
136
|
*/
|
|
51
|
-
|
|
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
|
+
});
|
|
52
145
|
/**
|
|
53
|
-
*
|
|
146
|
+
* Builds a key token.
|
|
147
|
+
*
|
|
148
|
+
* @param value - Key character or escape sequence
|
|
149
|
+
* @returns Frozen key token
|
|
54
150
|
*/
|
|
55
|
-
|
|
151
|
+
function keyToken(value) {
|
|
152
|
+
return index_cjs_js.freeze({ type: TokenType.Key, value });
|
|
153
|
+
}
|
|
56
154
|
/**
|
|
57
|
-
*
|
|
155
|
+
* Builds a paste token.
|
|
156
|
+
*
|
|
157
|
+
* @param value - Raw pasted text
|
|
158
|
+
* @returns Frozen paste token
|
|
58
159
|
*/
|
|
59
|
-
|
|
160
|
+
function pasteToken(value) {
|
|
161
|
+
return index_cjs_js.freeze({ type: TokenType.Paste, value });
|
|
162
|
+
}
|
|
60
163
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
|
63
173
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
183
|
/**
|
|
68
|
-
*
|
|
184
|
+
* Creates an incremental input tokenizer.
|
|
69
185
|
*
|
|
70
|
-
*
|
|
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
|
+
* ```
|
|
71
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
|
+
|
|
72
263
|
/**
|
|
73
264
|
* Key codes for terminal navigation.
|
|
74
265
|
*/
|
|
75
|
-
const Key = freeze({
|
|
266
|
+
const Key = index_cjs_js.freeze({
|
|
76
267
|
Up: '\x1B[A',
|
|
77
268
|
Down: '\x1B[B',
|
|
78
269
|
Left: '\x1B[D',
|
|
@@ -88,7 +279,7 @@ const Key = freeze({
|
|
|
88
279
|
/**
|
|
89
280
|
* ANSI escape codes for terminal styling.
|
|
90
281
|
*/
|
|
91
|
-
const Ansi = freeze({
|
|
282
|
+
const Ansi = index_cjs_js.freeze({
|
|
92
283
|
/** Clear entire line */
|
|
93
284
|
ClearLine: '\x1B[2K',
|
|
94
285
|
/** Move cursor to start of line */
|
|
@@ -114,6 +305,13 @@ const Ansi = freeze({
|
|
|
114
305
|
* @returns ANSI escape sequence string
|
|
115
306
|
*/
|
|
116
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`,
|
|
117
315
|
/** Escape code to hide cursor */
|
|
118
316
|
HideCursor: '\x1B[?25l',
|
|
119
317
|
/** Escape code to show cursor */
|
|
@@ -124,6 +322,10 @@ const Ansi = freeze({
|
|
|
124
322
|
RestoreCursor: '\x1B8',
|
|
125
323
|
/** Clear from cursor to end of screen */
|
|
126
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',
|
|
127
329
|
/** Escape code for bold text */
|
|
128
330
|
Bold: '\x1B[1m',
|
|
129
331
|
/** Escape code for dim text */
|
|
@@ -144,6 +346,12 @@ const Ansi = freeze({
|
|
|
144
346
|
/**
|
|
145
347
|
* Creates a terminal interface for interactive prompts.
|
|
146
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
|
+
*
|
|
147
355
|
* @param config - Terminal configuration options
|
|
148
356
|
* @returns Terminal interface with read/write methods
|
|
149
357
|
*
|
|
@@ -151,7 +359,7 @@ const Ansi = freeze({
|
|
|
151
359
|
* ```typescript
|
|
152
360
|
* const term = createTerminal()
|
|
153
361
|
* term.write('Enter name: ')
|
|
154
|
-
* const
|
|
362
|
+
* const token = await term.readToken()
|
|
155
363
|
* term.close()
|
|
156
364
|
* ```
|
|
157
365
|
*/
|
|
@@ -159,7 +367,15 @@ function createTerminal(config = {}) {
|
|
|
159
367
|
const input = config.input ?? process.stdin;
|
|
160
368
|
const output = config.output ?? process.stdout;
|
|
161
369
|
let cancelled = false;
|
|
370
|
+
let closed = false;
|
|
371
|
+
let sessionActive = false;
|
|
372
|
+
let savedRawMode = false;
|
|
162
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 = [];
|
|
163
379
|
const getReadline = () => {
|
|
164
380
|
if (!rl) {
|
|
165
381
|
rl = node_readline.createInterface({ input, output, terminal: true });
|
|
@@ -169,25 +385,78 @@ function createTerminal(config = {}) {
|
|
|
169
385
|
const write = (text) => {
|
|
170
386
|
output.write(text);
|
|
171
387
|
};
|
|
172
|
-
const
|
|
173
|
-
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;
|
|
174
422
|
if (input.setRawMode) {
|
|
175
423
|
input.setRawMode(true);
|
|
424
|
+
write(Ansi.BracketedPasteOn);
|
|
176
425
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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) => {
|
|
191
460
|
const readline = getReadline();
|
|
192
461
|
readline.once('line', (line) => {
|
|
193
462
|
resolve(line);
|
|
@@ -207,18 +476,28 @@ function createTerminal(config = {}) {
|
|
|
207
476
|
}
|
|
208
477
|
}
|
|
209
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
|
+
});
|
|
210
483
|
const close = () => {
|
|
484
|
+
if (closed)
|
|
485
|
+
return;
|
|
486
|
+
closed = true;
|
|
487
|
+
closeSession();
|
|
211
488
|
if (rl) {
|
|
212
489
|
rl.close();
|
|
213
490
|
rl = undefined;
|
|
214
491
|
}
|
|
215
492
|
write(Ansi.ShowCursor);
|
|
216
493
|
};
|
|
217
|
-
return freeze({
|
|
494
|
+
return index_cjs_js.freeze({
|
|
218
495
|
write,
|
|
219
496
|
readKey,
|
|
497
|
+
readToken,
|
|
220
498
|
readLine,
|
|
221
499
|
clearLines,
|
|
500
|
+
getSize,
|
|
222
501
|
close,
|
|
223
502
|
isCancelled: () => cancelled,
|
|
224
503
|
cancel: () => {
|
|
@@ -235,7 +514,7 @@ function createTerminal(config = {}) {
|
|
|
235
514
|
/**
|
|
236
515
|
* Unicode symbols for prompt rendering.
|
|
237
516
|
*/
|
|
238
|
-
const Symbol = freeze({
|
|
517
|
+
const Symbol = index_cjs_js.freeze({
|
|
239
518
|
Pointer: '❯',
|
|
240
519
|
Radio: '◯',
|
|
241
520
|
RadioSelected: '◉',
|
|
@@ -249,7 +528,7 @@ const Symbol = freeze({
|
|
|
249
528
|
/**
|
|
250
529
|
* Style functions for ANSI text formatting.
|
|
251
530
|
*/
|
|
252
|
-
const style = freeze({
|
|
531
|
+
const style = index_cjs_js.freeze({
|
|
253
532
|
/**
|
|
254
533
|
* Applies bold styling to text.
|
|
255
534
|
*
|
|
@@ -345,6 +624,139 @@ function renderCancelled() {
|
|
|
345
624
|
return style.dim('(cancelled)');
|
|
346
625
|
}
|
|
347
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
|
+
|
|
348
760
|
/**
|
|
349
761
|
* Core types for terminal prompts.
|
|
350
762
|
*
|
|
@@ -353,7 +765,7 @@ function renderCancelled() {
|
|
|
353
765
|
/**
|
|
354
766
|
* Result state of a prompt interaction.
|
|
355
767
|
*/
|
|
356
|
-
const PromptResult = freeze({
|
|
768
|
+
const PromptResult = index_cjs_js.freeze({
|
|
357
769
|
/** User submitted a value */
|
|
358
770
|
Submitted: 'submitted',
|
|
359
771
|
/** User cancelled the prompt (Ctrl+C) */
|
|
@@ -376,11 +788,36 @@ function renderOptions(initial) {
|
|
|
376
788
|
}
|
|
377
789
|
return style.dim('(y/n)');
|
|
378
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
|
+
}
|
|
379
814
|
/**
|
|
380
815
|
* Prompts for yes/no confirmation.
|
|
381
816
|
*
|
|
382
817
|
* Pure functional prompt that asks a yes/no question and returns a boolean.
|
|
383
|
-
* 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.
|
|
384
821
|
*
|
|
385
822
|
* @param config - Confirm prompt configuration
|
|
386
823
|
* @returns Promise resolving to boolean value or cancellation
|
|
@@ -403,40 +840,107 @@ function renderOptions(initial) {
|
|
|
403
840
|
*/
|
|
404
841
|
async function confirm(config) {
|
|
405
842
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
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 });
|
|
409
848
|
};
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
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) + ' ']) }));
|
|
413
851
|
};
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
}
|
|
428
872
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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;
|
|
433
907
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
908
|
+
const code = char.charCodeAt(0);
|
|
909
|
+
if (code < 0x20 || code === 0x7f)
|
|
910
|
+
continue;
|
|
911
|
+
if (pendingBreak && out.length > 0) {
|
|
912
|
+
out += ' ';
|
|
438
913
|
}
|
|
914
|
+
pendingBreak = false;
|
|
915
|
+
out += char;
|
|
439
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;
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
440
944
|
}
|
|
441
945
|
|
|
442
946
|
/**
|
|
@@ -473,7 +977,7 @@ function filterChoices$1(choices, query) {
|
|
|
473
977
|
indices.push(i);
|
|
474
978
|
}
|
|
475
979
|
});
|
|
476
|
-
return freeze(indices);
|
|
980
|
+
return index_cjs_js.freeze(indices);
|
|
477
981
|
}
|
|
478
982
|
/**
|
|
479
983
|
* Creates initial state for multiselect prompt.
|
|
@@ -483,7 +987,7 @@ function filterChoices$1(choices, query) {
|
|
|
483
987
|
* @returns Initial multiselect state
|
|
484
988
|
*/
|
|
485
989
|
function createInitialState$2(config) {
|
|
486
|
-
return freeze({
|
|
990
|
+
return index_cjs_js.freeze({
|
|
487
991
|
cursor: 0,
|
|
488
992
|
selected: config.initial ? [...config.initial] : [],
|
|
489
993
|
choices: config.choices,
|
|
@@ -543,72 +1047,75 @@ function renderChoice$1(choice, isSelected, isFocused) {
|
|
|
543
1047
|
return `${pointer} ${checkbox} ${label}${hint}`;
|
|
544
1048
|
}
|
|
545
1049
|
/**
|
|
546
|
-
*
|
|
1050
|
+
* Builds the frame lines for the multiselect prompt.
|
|
547
1051
|
*
|
|
548
1052
|
* @internal
|
|
549
|
-
* @param term - Terminal interface
|
|
550
1053
|
* @param config - Prompt configuration
|
|
551
1054
|
* @param state - Current prompt state
|
|
552
|
-
* @param
|
|
553
|
-
* @returns
|
|
1055
|
+
* @param maxVisible - Maximum number of visible choices
|
|
1056
|
+
* @returns Logical lines describing the prompt
|
|
554
1057
|
*/
|
|
555
|
-
function
|
|
556
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1058
|
+
function buildLines$1(config, state, maxVisible) {
|
|
557
1059
|
const { indices: visibleIndices, startIndex } = getVisibleChoices$1(state, maxVisible);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const selectedLabels = state.selected.map((i) => state.choices[i]?.label ?? '').join(', ');
|
|
561
|
-
output += renderSubmitted(selectedLabels || 'none');
|
|
562
|
-
term.write(output);
|
|
563
|
-
return 1;
|
|
564
|
-
}
|
|
1060
|
+
const lines = [];
|
|
1061
|
+
let header = renderMessage(config.message);
|
|
565
1062
|
if (config.searchable && state.searchQuery) {
|
|
566
|
-
|
|
1063
|
+
header += style.cyan(state.searchQuery) + style.dim(' (type to filter)');
|
|
567
1064
|
}
|
|
568
1065
|
else if (config.searchable) {
|
|
569
|
-
|
|
1066
|
+
header += style.dim('(type to filter, space to toggle, enter to submit)');
|
|
570
1067
|
}
|
|
571
1068
|
else {
|
|
572
|
-
|
|
1069
|
+
header += style.dim('(space to toggle, enter to submit)');
|
|
573
1070
|
}
|
|
574
|
-
|
|
575
|
-
let lineCount = 1;
|
|
1071
|
+
lines.push(header);
|
|
576
1072
|
const minMax = [];
|
|
577
1073
|
if (config.min !== undefined)
|
|
578
1074
|
minMax.push(`min: ${config.min}`);
|
|
579
1075
|
if (config.max !== undefined)
|
|
580
1076
|
minMax.push(`max: ${config.max}`);
|
|
581
1077
|
const countHint = minMax.length > 0 ? ` (${minMax.join(', ')})` : '';
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const showScrollDown = startIndex + maxVisible < state.filteredIndices.length;
|
|
586
|
-
if (showScrollUp) {
|
|
587
|
-
term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`) + '\n');
|
|
588
|
-
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)`));
|
|
589
1081
|
}
|
|
590
1082
|
visibleIndices.forEach((actualIndex, i) => {
|
|
591
1083
|
const choice = state.choices[actualIndex];
|
|
592
1084
|
/* istanbul ignore if -- @preserve defensive: actualIndex always valid from filteredIndices */
|
|
593
1085
|
if (!choice)
|
|
594
1086
|
return;
|
|
595
|
-
const
|
|
596
|
-
const isFocused = viewIndex === state.cursor;
|
|
1087
|
+
const isFocused = startIndex + i === state.cursor;
|
|
597
1088
|
const isSelected = arrayIncludes(state.selected, actualIndex);
|
|
598
|
-
|
|
599
|
-
term.write(Ansi.ClearLine + line + '\n');
|
|
600
|
-
lineCount++;
|
|
1089
|
+
lines.push(renderChoice$1(choice, isSelected, isFocused));
|
|
601
1090
|
});
|
|
602
|
-
if (
|
|
1091
|
+
if (startIndex + maxVisible < state.filteredIndices.length) {
|
|
603
1092
|
const remaining = state.filteredIndices.length - (startIndex + maxVisible);
|
|
604
|
-
|
|
605
|
-
lineCount++;
|
|
1093
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${remaining} more below)`));
|
|
606
1094
|
}
|
|
607
1095
|
if (state.filteredIndices.length === 0 && config.searchable) {
|
|
608
|
-
|
|
609
|
-
lineCount++;
|
|
1096
|
+
lines.push(style.dim(' No matches found'));
|
|
610
1097
|
}
|
|
611
|
-
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
|
+
});
|
|
612
1119
|
}
|
|
613
1120
|
/**
|
|
614
1121
|
* Processes a keypress and returns updated state.
|
|
@@ -617,10 +1124,10 @@ function render$2(term, config, state, submitted) {
|
|
|
617
1124
|
* @param key - The key that was pressed
|
|
618
1125
|
* @param state - Current prompt state
|
|
619
1126
|
* @param config - Prompt configuration
|
|
1127
|
+
* @param maxVisible - Maximum number of visible choices
|
|
620
1128
|
* @returns Updated state after processing the key
|
|
621
1129
|
*/
|
|
622
|
-
function processKey$2(key, state, config) {
|
|
623
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1130
|
+
function processKey$2(key, state, config, maxVisible) {
|
|
624
1131
|
const total = state.filteredIndices.length;
|
|
625
1132
|
if (total === 0 && key !== Key.Backspace && key !== '\b')
|
|
626
1133
|
return state;
|
|
@@ -635,7 +1142,7 @@ function processKey$2(key, state, config) {
|
|
|
635
1142
|
if (newCursor < state.scrollOffset) {
|
|
636
1143
|
newScrollOffset = newCursor;
|
|
637
1144
|
}
|
|
638
|
-
return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
1145
|
+
return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
639
1146
|
}
|
|
640
1147
|
if (key === Key.Down) {
|
|
641
1148
|
let newCursor = state.cursor + 1;
|
|
@@ -648,7 +1155,7 @@ function processKey$2(key, state, config) {
|
|
|
648
1155
|
if (newCursor >= state.scrollOffset + maxVisible) {
|
|
649
1156
|
newScrollOffset = newCursor - maxVisible + 1;
|
|
650
1157
|
}
|
|
651
|
-
return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
1158
|
+
return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
652
1159
|
}
|
|
653
1160
|
if (key === Key.Space) {
|
|
654
1161
|
const actualIndex = state.filteredIndices[state.cursor];
|
|
@@ -661,14 +1168,14 @@ function processKey$2(key, state, config) {
|
|
|
661
1168
|
const isSelected = arrayIncludes(state.selected, actualIndex);
|
|
662
1169
|
if (isSelected) {
|
|
663
1170
|
const newSelected = state.selected.filter((i) => i !== actualIndex);
|
|
664
|
-
return freeze({ ...state, selected: newSelected });
|
|
1171
|
+
return index_cjs_js.freeze({ ...state, selected: newSelected });
|
|
665
1172
|
}
|
|
666
1173
|
else {
|
|
667
1174
|
if (config.max !== undefined && state.selected.length >= config.max) {
|
|
668
1175
|
return state;
|
|
669
1176
|
}
|
|
670
1177
|
const newSelected = [...state.selected, actualIndex];
|
|
671
|
-
return freeze({ ...state, selected: newSelected });
|
|
1178
|
+
return index_cjs_js.freeze({ ...state, selected: newSelected });
|
|
672
1179
|
}
|
|
673
1180
|
}
|
|
674
1181
|
if (config.searchable) {
|
|
@@ -676,7 +1183,7 @@ function processKey$2(key, state, config) {
|
|
|
676
1183
|
if (state.searchQuery.length > 0) {
|
|
677
1184
|
const newQuery = state.searchQuery.slice(0, -1);
|
|
678
1185
|
const newFiltered = filterChoices$1(state.choices, newQuery);
|
|
679
|
-
return freeze({
|
|
1186
|
+
return index_cjs_js.freeze({
|
|
680
1187
|
...state,
|
|
681
1188
|
searchQuery: newQuery,
|
|
682
1189
|
filteredIndices: newFiltered,
|
|
@@ -687,15 +1194,7 @@ function processKey$2(key, state, config) {
|
|
|
687
1194
|
return state;
|
|
688
1195
|
}
|
|
689
1196
|
if (key.length === 1 && key >= ' ' && key !== Key.Space) {
|
|
690
|
-
|
|
691
|
-
const newFiltered = filterChoices$1(state.choices, newQuery);
|
|
692
|
-
return freeze({
|
|
693
|
-
...state,
|
|
694
|
-
searchQuery: newQuery,
|
|
695
|
-
filteredIndices: newFiltered,
|
|
696
|
-
cursor: 0,
|
|
697
|
-
scrollOffset: 0,
|
|
698
|
-
});
|
|
1197
|
+
return appendSearch$1(key, state);
|
|
699
1198
|
}
|
|
700
1199
|
}
|
|
701
1200
|
return state;
|
|
@@ -717,11 +1216,26 @@ function validateSelection(state, config) {
|
|
|
717
1216
|
}
|
|
718
1217
|
return undefined;
|
|
719
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
|
+
}
|
|
720
1231
|
/**
|
|
721
1232
|
* Prompts for multiple selections from a list of choices.
|
|
722
1233
|
*
|
|
723
1234
|
* Pure functional prompt with arrow key navigation, space to toggle,
|
|
724
|
-
* 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.
|
|
725
1239
|
*
|
|
726
1240
|
* @param config - Multiselect prompt configuration
|
|
727
1241
|
* @returns Promise resolving to array of selected values or cancellation
|
|
@@ -763,55 +1277,59 @@ function validateSelection(state, config) {
|
|
|
763
1277
|
*/
|
|
764
1278
|
async function multiselect(config) {
|
|
765
1279
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1280
|
+
const screen = createScreen(term);
|
|
766
1281
|
let state = createInitialState$2(config);
|
|
767
|
-
let lineCount = 0;
|
|
768
1282
|
let errorMessage;
|
|
769
1283
|
term.write(Ansi.HideCursor);
|
|
770
|
-
const redraw = (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
term.write(Ansi.ClearToEnd);
|
|
775
|
-
lineCount = render$2(term, config, state, submitted);
|
|
776
|
-
if (errorMessage && !submitted) {
|
|
777
|
-
term.write(Ansi.ClearLine + style.yellow(` ${errorMessage}`) + '\n');
|
|
778
|
-
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}`));
|
|
779
1288
|
}
|
|
1289
|
+
screen.render(index_cjs_js.freeze({ lines: index_cjs_js.freeze(lines) }));
|
|
780
1290
|
};
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (
|
|
786
|
-
|
|
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 });
|
|
787
1299
|
}
|
|
788
|
-
|
|
789
|
-
term.write(renderMessage(config.message) + renderCancelled() + '\n');
|
|
790
|
-
term.write(Ansi.ShowCursor);
|
|
791
|
-
term.close();
|
|
792
|
-
return freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
793
|
-
}
|
|
794
|
-
if (key === Key.Enter) {
|
|
795
|
-
const validationError = validateSelection(state, config);
|
|
796
|
-
if (validationError) {
|
|
797
|
-
errorMessage = validationError;
|
|
1300
|
+
if (token.type === TokenType.Resize) {
|
|
798
1301
|
redraw();
|
|
799
1302
|
continue;
|
|
800
1303
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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) });
|
|
804
1325
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
term.write(Ansi.ShowCursor);
|
|
809
|
-
term.close();
|
|
810
|
-
return freeze({ result: PromptResult.Submitted, value: freeze(selectedValues) });
|
|
1326
|
+
errorMessage = undefined;
|
|
1327
|
+
state = processKey$2(token.value, state, config, effectiveMaxVisible$1(term, config.maxVisible));
|
|
1328
|
+
redraw();
|
|
811
1329
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1330
|
+
}
|
|
1331
|
+
finally {
|
|
1332
|
+
term.close();
|
|
815
1333
|
}
|
|
816
1334
|
}
|
|
817
1335
|
|
|
@@ -834,7 +1352,7 @@ function filterChoices(choices, query) {
|
|
|
834
1352
|
indices.push(i);
|
|
835
1353
|
}
|
|
836
1354
|
});
|
|
837
|
-
return freeze(indices);
|
|
1355
|
+
return index_cjs_js.freeze(indices);
|
|
838
1356
|
}
|
|
839
1357
|
/**
|
|
840
1358
|
* Creates initial state for select prompt.
|
|
@@ -844,7 +1362,7 @@ function filterChoices(choices, query) {
|
|
|
844
1362
|
* @returns Initial select state
|
|
845
1363
|
*/
|
|
846
1364
|
function createInitialState$1(config) {
|
|
847
|
-
return freeze({
|
|
1365
|
+
return index_cjs_js.freeze({
|
|
848
1366
|
cursor: config.initial ?? 0,
|
|
849
1367
|
choices: config.choices,
|
|
850
1368
|
filteredIndices: config.choices.map((_, i) => i),
|
|
@@ -898,65 +1416,66 @@ function renderChoice(choice, isFocused) {
|
|
|
898
1416
|
return `${pointer} ${label}${hint}`;
|
|
899
1417
|
}
|
|
900
1418
|
/**
|
|
901
|
-
*
|
|
1419
|
+
* Builds the frame lines for the select prompt.
|
|
902
1420
|
*
|
|
903
1421
|
* @internal
|
|
904
|
-
* @param term - Terminal interface
|
|
905
1422
|
* @param config - Prompt configuration
|
|
906
1423
|
* @param state - Current prompt state
|
|
907
|
-
* @param
|
|
908
|
-
* @returns
|
|
1424
|
+
* @param maxVisible - Maximum number of visible choices
|
|
1425
|
+
* @returns Logical lines describing the prompt
|
|
909
1426
|
*/
|
|
910
|
-
function
|
|
911
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1427
|
+
function buildLines(config, state, maxVisible) {
|
|
912
1428
|
const { indices: visibleIndices, startIndex } = getVisibleChoices(state, maxVisible);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const actualIndex = state.filteredIndices[state.cursor];
|
|
916
|
-
const selectedChoice = actualIndex !== undefined ? state.choices[actualIndex] : undefined;
|
|
917
|
-
/* istanbul ignore next -- @preserve defensive: cursor always within bounds */
|
|
918
|
-
output += renderSubmitted(selectedChoice?.label ?? '');
|
|
919
|
-
term.write(output);
|
|
920
|
-
return 1;
|
|
921
|
-
}
|
|
1429
|
+
const lines = [];
|
|
1430
|
+
let header = renderMessage(config.message);
|
|
922
1431
|
if (config.searchable && state.searchQuery) {
|
|
923
|
-
|
|
1432
|
+
header += style.cyan(state.searchQuery) + style.dim(' (type to filter)');
|
|
924
1433
|
}
|
|
925
1434
|
else if (config.searchable) {
|
|
926
|
-
|
|
1435
|
+
header += style.dim('(type to filter, enter to select)');
|
|
927
1436
|
}
|
|
928
1437
|
else {
|
|
929
|
-
|
|
1438
|
+
header += style.dim('(use arrows, enter to select)');
|
|
930
1439
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const showScrollDown = startIndex + maxVisible < state.filteredIndices.length;
|
|
935
|
-
if (showScrollUp) {
|
|
936
|
-
term.write(Ansi.ClearLine + style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`) + '\n');
|
|
937
|
-
lineCount++;
|
|
1440
|
+
lines.push(header);
|
|
1441
|
+
if (startIndex > 0) {
|
|
1442
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${startIndex} more above)`));
|
|
938
1443
|
}
|
|
939
1444
|
visibleIndices.forEach((actualIndex, i) => {
|
|
940
1445
|
const choice = state.choices[actualIndex];
|
|
941
1446
|
/* istanbul ignore if -- @preserve defensive: actualIndex always valid from filteredIndices */
|
|
942
1447
|
if (!choice)
|
|
943
1448
|
return;
|
|
944
|
-
|
|
945
|
-
const isFocused = viewIndex === state.cursor;
|
|
946
|
-
const line = renderChoice(choice, isFocused);
|
|
947
|
-
term.write(Ansi.ClearLine + line + '\n');
|
|
948
|
-
lineCount++;
|
|
1449
|
+
lines.push(renderChoice(choice, startIndex + i === state.cursor));
|
|
949
1450
|
});
|
|
950
|
-
if (
|
|
1451
|
+
if (startIndex + maxVisible < state.filteredIndices.length) {
|
|
951
1452
|
const remaining = state.filteredIndices.length - (startIndex + maxVisible);
|
|
952
|
-
|
|
953
|
-
lineCount++;
|
|
1453
|
+
lines.push(style.dim(` ${Symbol.Ellipsis} (${remaining} more below)`));
|
|
954
1454
|
}
|
|
955
1455
|
if (state.filteredIndices.length === 0 && config.searchable) {
|
|
956
|
-
|
|
957
|
-
lineCount++;
|
|
1456
|
+
lines.push(style.dim(' No matches found'));
|
|
958
1457
|
}
|
|
959
|
-
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
|
+
});
|
|
960
1479
|
}
|
|
961
1480
|
/**
|
|
962
1481
|
* Processes a keypress and returns updated state.
|
|
@@ -965,10 +1484,10 @@ function render$1(term, config, state, submitted) {
|
|
|
965
1484
|
* @param key - The key that was pressed
|
|
966
1485
|
* @param state - Current prompt state
|
|
967
1486
|
* @param config - Prompt configuration
|
|
1487
|
+
* @param maxVisible - Maximum number of visible choices
|
|
968
1488
|
* @returns Updated state after processing the key
|
|
969
1489
|
*/
|
|
970
|
-
function processKey$1(key, state, config) {
|
|
971
|
-
const maxVisible = config.maxVisible ?? 10;
|
|
1490
|
+
function processKey$1(key, state, config, maxVisible) {
|
|
972
1491
|
const total = state.filteredIndices.length;
|
|
973
1492
|
if (total === 0 && key !== Key.Backspace && key !== '\b')
|
|
974
1493
|
return state;
|
|
@@ -983,7 +1502,7 @@ function processKey$1(key, state, config) {
|
|
|
983
1502
|
if (newCursor < state.scrollOffset) {
|
|
984
1503
|
newScrollOffset = newCursor;
|
|
985
1504
|
}
|
|
986
|
-
return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
1505
|
+
return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
987
1506
|
}
|
|
988
1507
|
if (key === Key.Down) {
|
|
989
1508
|
let newCursor = state.cursor + 1;
|
|
@@ -996,14 +1515,14 @@ function processKey$1(key, state, config) {
|
|
|
996
1515
|
if (newCursor >= state.scrollOffset + maxVisible) {
|
|
997
1516
|
newScrollOffset = newCursor - maxVisible + 1;
|
|
998
1517
|
}
|
|
999
|
-
return freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
1518
|
+
return index_cjs_js.freeze({ ...state, cursor: newCursor, scrollOffset: newScrollOffset });
|
|
1000
1519
|
}
|
|
1001
1520
|
if (config.searchable) {
|
|
1002
1521
|
if (key === Key.Backspace || key === '\b') {
|
|
1003
1522
|
if (state.searchQuery.length > 0) {
|
|
1004
1523
|
const newQuery = state.searchQuery.slice(0, -1);
|
|
1005
1524
|
const newFiltered = filterChoices(state.choices, newQuery);
|
|
1006
|
-
return freeze({
|
|
1525
|
+
return index_cjs_js.freeze({
|
|
1007
1526
|
...state,
|
|
1008
1527
|
searchQuery: newQuery,
|
|
1009
1528
|
filteredIndices: newFiltered,
|
|
@@ -1014,15 +1533,7 @@ function processKey$1(key, state, config) {
|
|
|
1014
1533
|
return state;
|
|
1015
1534
|
}
|
|
1016
1535
|
if (key.length === 1 && key >= ' ') {
|
|
1017
|
-
|
|
1018
|
-
const newFiltered = filterChoices(state.choices, newQuery);
|
|
1019
|
-
return freeze({
|
|
1020
|
-
...state,
|
|
1021
|
-
searchQuery: newQuery,
|
|
1022
|
-
filteredIndices: newFiltered,
|
|
1023
|
-
cursor: 0,
|
|
1024
|
-
scrollOffset: 0,
|
|
1025
|
-
});
|
|
1536
|
+
return appendSearch(key, state);
|
|
1026
1537
|
}
|
|
1027
1538
|
}
|
|
1028
1539
|
return state;
|
|
@@ -1031,7 +1542,10 @@ function processKey$1(key, state, config) {
|
|
|
1031
1542
|
* Prompts for single selection from a list of choices.
|
|
1032
1543
|
*
|
|
1033
1544
|
* Pure functional prompt with arrow key navigation, scrolling support,
|
|
1034
|
-
* 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.
|
|
1035
1549
|
*
|
|
1036
1550
|
* @param config - Select prompt configuration
|
|
1037
1551
|
* @returns Promise resolving to selected value or cancellation
|
|
@@ -1075,70 +1589,66 @@ function processKey$1(key, state, config) {
|
|
|
1075
1589
|
*/
|
|
1076
1590
|
async function select(config) {
|
|
1077
1591
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1592
|
+
const screen = createScreen(term);
|
|
1078
1593
|
let state = createInitialState$1(config);
|
|
1079
|
-
let lineCount = 0;
|
|
1080
1594
|
term.write(Ansi.HideCursor);
|
|
1081
|
-
const redraw = (
|
|
1082
|
-
|
|
1083
|
-
term.write(Ansi.cursorUp(lineCount) + Ansi.CursorStart);
|
|
1084
|
-
}
|
|
1085
|
-
term.write(Ansi.ClearToEnd);
|
|
1086
|
-
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)) }));
|
|
1087
1597
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
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 });
|
|
1094
1606
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
term.write(Ansi.ShowCursor);
|
|
1098
|
-
term.close();
|
|
1099
|
-
return freeze({ result: PromptResult.Cancelled, value: undefined });
|
|
1100
|
-
}
|
|
1101
|
-
if (key === Key.Enter) {
|
|
1102
|
-
const actualIndex = state.filteredIndices[state.cursor];
|
|
1103
|
-
if (actualIndex === undefined) {
|
|
1607
|
+
if (token.type === TokenType.Resize) {
|
|
1608
|
+
redraw();
|
|
1104
1609
|
continue;
|
|
1105
1610
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1611
|
+
if (token.type === TokenType.Paste) {
|
|
1612
|
+
if (config.searchable) {
|
|
1613
|
+
state = appendSearch(firstPasteLine(token.value), state);
|
|
1614
|
+
redraw();
|
|
1615
|
+
}
|
|
1108
1616
|
continue;
|
|
1109
1617
|
}
|
|
1110
|
-
if (
|
|
1111
|
-
|
|
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 });
|
|
1112
1631
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
term.write('\n');
|
|
1116
|
-
term.write(Ansi.ShowCursor);
|
|
1117
|
-
term.close();
|
|
1118
|
-
return freeze({ result: PromptResult.Submitted, value: selectedChoice.value });
|
|
1632
|
+
state = processKey$1(token.value, state, config, effectiveMaxVisible(term, config.maxVisible));
|
|
1633
|
+
redraw();
|
|
1119
1634
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1635
|
+
}
|
|
1636
|
+
finally {
|
|
1637
|
+
term.close();
|
|
1122
1638
|
}
|
|
1123
1639
|
}
|
|
1124
|
-
|
|
1125
1640
|
/**
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
* These references are captured at module initialization time to protect against
|
|
1129
|
-
* prototype pollution attacks. Import only what you need for tree-shaking.
|
|
1641
|
+
* Resolves the visible-window size, capped so the frame fits the terminal
|
|
1642
|
+
* height (header and scroll-indicator rows reserved).
|
|
1130
1643
|
*
|
|
1131
|
-
* @
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
* (Safe copy) Returns the larger of zero or more numbers.
|
|
1136
|
-
*/
|
|
1137
|
-
const max = _Math.max;
|
|
1138
|
-
/**
|
|
1139
|
-
* (Safe copy) Returns the smaller of zero or more numbers.
|
|
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)
|
|
1140
1648
|
*/
|
|
1141
|
-
|
|
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
|
+
}
|
|
1142
1652
|
|
|
1143
1653
|
/**
|
|
1144
1654
|
* Creates initial state for text prompt.
|
|
@@ -1155,40 +1665,63 @@ function createInitialState(config) {
|
|
|
1155
1665
|
};
|
|
1156
1666
|
}
|
|
1157
1667
|
/**
|
|
1158
|
-
*
|
|
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.
|
|
1159
1680
|
*
|
|
1160
1681
|
* @internal
|
|
1161
|
-
* @param term - Terminal interface for output
|
|
1162
1682
|
* @param config - Prompt configuration
|
|
1163
1683
|
* @param state - Current prompt state
|
|
1164
1684
|
* @param submitted - Whether the prompt has been submitted
|
|
1165
|
-
* @returns
|
|
1685
|
+
* @returns Frame describing lines and cursor position
|
|
1166
1686
|
*/
|
|
1167
|
-
function
|
|
1687
|
+
function buildFrame(config, state, submitted) {
|
|
1168
1688
|
const displayValue = config.format ? config.format(state.value) : state.value;
|
|
1169
|
-
|
|
1170
|
-
let output = renderMessage(messageText);
|
|
1171
|
-
let trailingChars = 0;
|
|
1689
|
+
let line = renderMessage(messageText(config, state.value));
|
|
1172
1690
|
if (submitted) {
|
|
1173
|
-
|
|
1691
|
+
line += renderSubmitted(displayValue || config.initial || '');
|
|
1692
|
+
return index_cjs_js.freeze({ lines: index_cjs_js.freeze([line]) });
|
|
1174
1693
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
trailingChars += config.initial.length;
|
|
1181
|
-
}
|
|
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);
|
|
1182
1699
|
}
|
|
1183
|
-
|
|
1700
|
+
const cursor = index_cjs_js.freeze({ line: 0, col: displayWidth(line) - trailingWidth });
|
|
1184
1701
|
if (state.error) {
|
|
1185
|
-
|
|
1186
|
-
return 2;
|
|
1187
|
-
}
|
|
1188
|
-
if (!submitted && trailingChars > 0) {
|
|
1189
|
-
term.write(Ansi.cursorLeft(trailingChars));
|
|
1702
|
+
return index_cjs_js.freeze({ lines: index_cjs_js.freeze([line, style.yellow(` ${state.error}`)]), cursor });
|
|
1190
1703
|
}
|
|
1191
|
-
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
|
+
};
|
|
1192
1725
|
}
|
|
1193
1726
|
/**
|
|
1194
1727
|
* Processes a keypress and returns updated state.
|
|
@@ -1200,14 +1733,7 @@ function render(term, config, state, submitted) {
|
|
|
1200
1733
|
*/
|
|
1201
1734
|
function processKey(key, state) {
|
|
1202
1735
|
if (key.length === 1 && key >= ' ' && key !== Key.Backspace) {
|
|
1203
|
-
|
|
1204
|
-
const after = state.value.slice(state.cursorPos);
|
|
1205
|
-
return {
|
|
1206
|
-
...state,
|
|
1207
|
-
value: before + key + after,
|
|
1208
|
-
cursorPos: state.cursorPos + 1,
|
|
1209
|
-
error: undefined,
|
|
1210
|
-
};
|
|
1736
|
+
return insertText(key, state);
|
|
1211
1737
|
}
|
|
1212
1738
|
if (key === Key.Backspace || key === '\b') {
|
|
1213
1739
|
if (state.cursorPos > 0) {
|
|
@@ -1224,13 +1750,13 @@ function processKey(key, state) {
|
|
|
1224
1750
|
if (key === Key.Left) {
|
|
1225
1751
|
return {
|
|
1226
1752
|
...state,
|
|
1227
|
-
cursorPos: max(0, state.cursorPos - 1),
|
|
1753
|
+
cursorPos: index_cjs_js$1.max(0, state.cursorPos - 1),
|
|
1228
1754
|
};
|
|
1229
1755
|
}
|
|
1230
1756
|
if (key === Key.Right) {
|
|
1231
1757
|
return {
|
|
1232
1758
|
...state,
|
|
1233
|
-
cursorPos: min(state.value.length, state.cursorPos + 1),
|
|
1759
|
+
cursorPos: index_cjs_js$1.min(state.value.length, state.cursorPos + 1),
|
|
1234
1760
|
};
|
|
1235
1761
|
}
|
|
1236
1762
|
return state;
|
|
@@ -1239,7 +1765,11 @@ function processKey(key, state) {
|
|
|
1239
1765
|
* Prompts for text input with optional validation.
|
|
1240
1766
|
*
|
|
1241
1767
|
* Pure functional prompt that reads text from the user with support for
|
|
1242
|
-
* 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.
|
|
1243
1773
|
*
|
|
1244
1774
|
* @param config - Text prompt configuration
|
|
1245
1775
|
* @returns Promise resolving to submitted value or cancellation
|
|
@@ -1273,40 +1803,49 @@ function processKey(key, state) {
|
|
|
1273
1803
|
*/
|
|
1274
1804
|
async function text(config) {
|
|
1275
1805
|
const term = createTerminal({ input: config.input, output: config.output });
|
|
1806
|
+
const screen = createScreen(term);
|
|
1276
1807
|
let state = createInitialState(config);
|
|
1277
|
-
let lineCount = 0;
|
|
1278
1808
|
const redraw = (submitted = false) => {
|
|
1279
|
-
|
|
1280
|
-
term.clearLines(lineCount);
|
|
1281
|
-
}
|
|
1282
|
-
lineCount = render(term, config, state, submitted);
|
|
1809
|
+
screen.render(buildFrame(config, state, submitted));
|
|
1283
1810
|
};
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
+
}
|
|
1301
1838
|
}
|
|
1839
|
+
redraw(true);
|
|
1840
|
+
term.write('\n');
|
|
1841
|
+
return index_cjs_js.freeze({ result: PromptResult.Submitted, value });
|
|
1302
1842
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
term.close();
|
|
1306
|
-
return freeze({ result: PromptResult.Submitted, value });
|
|
1843
|
+
state = processKey(token.value, state);
|
|
1844
|
+
redraw();
|
|
1307
1845
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1846
|
+
}
|
|
1847
|
+
finally {
|
|
1848
|
+
term.close();
|
|
1310
1849
|
}
|
|
1311
1850
|
}
|
|
1312
1851
|
|
|
@@ -1316,4 +1855,3 @@ exports.multiselect = multiselect;
|
|
|
1316
1855
|
exports.select = select;
|
|
1317
1856
|
exports.style = style;
|
|
1318
1857
|
exports.text = text;
|
|
1319
|
-
//# sourceMappingURL=index.cjs.js.map
|