@topcli/prompts 1.3.1 → 1.4.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/README.md +34 -2
- package/index.d.ts +8 -1
- package/index.js +7 -0
- package/package.json +6 -6
- package/src/abstract-prompt.js +1 -1
- package/src/constants.js +9 -3
- package/src/multiselect-prompt.js +257 -0
- package/src/validators.js +1 -1
package/README.md
CHANGED
|
@@ -27,15 +27,19 @@ $ yarn add @topcli/prompts
|
|
|
27
27
|
You can locally run `node ./demo.js`
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
import { question, confirm, select } from "@topcli/prompts";
|
|
30
|
+
import { question, confirm, select, multiselect } from "@topcli/prompts";
|
|
31
31
|
|
|
32
32
|
const kTestRunner = ["node", "tap", "tape", "vitest", "mocha", "ava"];
|
|
33
33
|
|
|
34
34
|
const name = await question("Project name ?", { defaultValue: "foo" });
|
|
35
35
|
const runner = await select("Choose a test runner", { choices: kTestRunner, maxVisible: 5 });
|
|
36
36
|
const isCLI = await confirm("Your project is a CLI ?", { initial: true });
|
|
37
|
+
const os = await multiselect("Choose OS", {
|
|
38
|
+
choices: ["linux", "mac", "windows"],
|
|
39
|
+
preSelectedChoices: ["linux"]
|
|
40
|
+
});
|
|
37
41
|
|
|
38
|
-
console.log(name, runner, isCLI);
|
|
42
|
+
console.log(name, runner, isCLI, os);
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
## API
|
|
@@ -83,6 +87,26 @@ select(message: string, options: SelectOptions): Promise<string>
|
|
|
83
87
|
Scrollable select depending `maxVisible` (default `8`).
|
|
84
88
|
Use `ignoreValues` to skip result render & clear lines after a selected one.
|
|
85
89
|
|
|
90
|
+
### `multiselect()`
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
multiselect(message: string, options: MultiselectOptions): Promise<[string]>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Scrollable multiselect depending `maxVisible` (default `8`).
|
|
97
|
+
Use `preSelectedChoices` to pre-select choices.
|
|
98
|
+
|
|
99
|
+
Use `validators` to handle user input.
|
|
100
|
+
|
|
101
|
+
**Example**
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
const os = await multiselect('Choose OS', {
|
|
105
|
+
choices: ["linux", "mac", "windows"]
|
|
106
|
+
validators: [required()]
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
86
110
|
### `confirm()`
|
|
87
111
|
|
|
88
112
|
```ts
|
|
@@ -109,6 +133,7 @@ assert.equal(input, "John");
|
|
|
109
133
|
> - When using `question()`, `validators` functions will not be executed.
|
|
110
134
|
> - When using `select()`, the answer can be different from the available choices.
|
|
111
135
|
> - When using `confirm()`, the answer can be any type other than boolean.
|
|
136
|
+
> - etc
|
|
112
137
|
> **Use with caution**
|
|
113
138
|
|
|
114
139
|
## Interfaces
|
|
@@ -145,6 +170,13 @@ export interface SelectOptions extends SharedOptions {
|
|
|
145
170
|
ignoreValues?: (string | number | boolean)[];
|
|
146
171
|
}
|
|
147
172
|
|
|
173
|
+
export interface MultiselectOptions extends SharedOptions {
|
|
174
|
+
choices: (Choice | string)[];
|
|
175
|
+
maxVisible?: number;
|
|
176
|
+
preSelectedChoices?: (Choice | string)[];
|
|
177
|
+
validators?: Validator[];
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
export interface ConfirmOptions extends SharedOptions {
|
|
149
181
|
initial?: boolean;
|
|
150
182
|
}
|
package/index.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ export interface QuestionOptions extends SharedOptions {
|
|
|
17
17
|
validators?: Validator[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
20
|
export interface Choice {
|
|
22
21
|
value: any;
|
|
23
22
|
label: string;
|
|
@@ -30,12 +29,20 @@ export interface SelectOptions extends SharedOptions {
|
|
|
30
29
|
ignoreValues?: (string | number | boolean)[];
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
export interface MultiselectOptions extends SharedOptions {
|
|
33
|
+
choices: (Choice | string)[];
|
|
34
|
+
maxVisible?: number;
|
|
35
|
+
preSelectedChoices?: (Choice | string)[];
|
|
36
|
+
validators?: Validator[];
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export interface ConfirmOptions extends SharedOptions {
|
|
34
40
|
initial?: boolean;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function question(message: string, options?: QuestionOptions): Promise<string>;
|
|
38
44
|
export function select(message: string, options: SelectOptions): Promise<string>;
|
|
45
|
+
export function multiselect(message: string, options: MultiselectOptions): Promise<string[]>;
|
|
39
46
|
export function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
40
47
|
|
|
41
48
|
export function required(): Validator;
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SelectPrompt } from "./src/select-prompt.js";
|
|
|
4
4
|
import { QuestionPrompt } from "./src/question-prompt.js";
|
|
5
5
|
import { required } from "./src/validators.js";
|
|
6
6
|
import { PromptAgent } from "./src/prompt-agent.js";
|
|
7
|
+
import { MultiselectPrompt } from "./src/multiselect-prompt.js";
|
|
7
8
|
|
|
8
9
|
export async function question(message, options = {}) {
|
|
9
10
|
const questionPrompt = new QuestionPrompt(message, options);
|
|
@@ -23,4 +24,10 @@ export async function confirm(message, options) {
|
|
|
23
24
|
return confirmPrompt.confirm();
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export async function multiselect(message, options) {
|
|
28
|
+
const multiselectPrompt = new MultiselectPrompt(message, options);
|
|
29
|
+
|
|
30
|
+
return multiselectPrompt.multiselect();
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export { required, PromptAgent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topcli/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Node.js user input library for command-line interfaces.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --no-warnings=ExperimentalWarning --loader=esmock --test test/",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"license": "ISC",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@nodesecure/eslint-config": "^1.
|
|
27
|
-
"@types/node": "^20.
|
|
28
|
-
"c8": "^8.0.
|
|
29
|
-
"eslint": "^8.
|
|
30
|
-
"esmock": "^2.
|
|
26
|
+
"@nodesecure/eslint-config": "^1.8.0",
|
|
27
|
+
"@types/node": "^20.8.2",
|
|
28
|
+
"c8": "^8.0.1",
|
|
29
|
+
"eslint": "^8.50.0",
|
|
30
|
+
"esmock": "^2.5.1"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"is-unicode-supported": "^1.3.0",
|
package/src/abstract-prompt.js
CHANGED
|
@@ -47,7 +47,7 @@ export class AbstractPrompt {
|
|
|
47
47
|
|
|
48
48
|
const lastLineRows = Math.ceil(stripAnsi(lastLine).length / this.stdout.columns);
|
|
49
49
|
|
|
50
|
-
this.stdout.moveCursor(
|
|
50
|
+
this.stdout.moveCursor(-this.stdout.columns, -lastLineRows);
|
|
51
51
|
this.stdout.clearScreenDown();
|
|
52
52
|
}
|
|
53
53
|
|
package/src/constants.js
CHANGED
|
@@ -7,14 +7,18 @@ const kMainSymbols = {
|
|
|
7
7
|
cross: "✖",
|
|
8
8
|
pointer: "›",
|
|
9
9
|
previous: "⭡",
|
|
10
|
-
next: "⭣"
|
|
10
|
+
next: "⭣",
|
|
11
|
+
active: "●",
|
|
12
|
+
inactive: "○"
|
|
11
13
|
};
|
|
12
14
|
const kFallbackSymbols = {
|
|
13
15
|
tick: "√",
|
|
14
16
|
cross: "×",
|
|
15
17
|
pointer: ">",
|
|
16
18
|
previous: "↑",
|
|
17
|
-
next: "↓"
|
|
19
|
+
next: "↓",
|
|
20
|
+
active: "(+)",
|
|
21
|
+
inactive: "(-)"
|
|
18
22
|
};
|
|
19
23
|
const kSymbols = isUnicodeSupported() ? kMainSymbols : kFallbackSymbols;
|
|
20
24
|
const kPointer = kleur.gray(kSymbols.pointer);
|
|
@@ -27,5 +31,7 @@ export const SYMBOLS = {
|
|
|
27
31
|
Previous: kSymbols.previous,
|
|
28
32
|
Next: kSymbols.next,
|
|
29
33
|
ShowCursor: "\x1B[?25h",
|
|
30
|
-
HideCursor: "\x1B[?25l"
|
|
34
|
+
HideCursor: "\x1B[?25l",
|
|
35
|
+
Active: kleur.cyan(kSymbols.active),
|
|
36
|
+
Inactive: kleur.gray(kSymbols.inactive)
|
|
31
37
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
|
|
4
|
+
// Import Third-party Dependencies
|
|
5
|
+
import kleur from "kleur";
|
|
6
|
+
|
|
7
|
+
// Import Internal Dependencies
|
|
8
|
+
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
9
|
+
import { SYMBOLS } from "./constants.js";
|
|
10
|
+
|
|
11
|
+
export class MultiselectPrompt extends AbstractPrompt {
|
|
12
|
+
#validators;
|
|
13
|
+
|
|
14
|
+
activeIndex = 0;
|
|
15
|
+
selectedIndexes = [];
|
|
16
|
+
|
|
17
|
+
get choices() {
|
|
18
|
+
return this.options.choices;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
const {
|
|
23
|
+
stdin = process.stdin,
|
|
24
|
+
stdout = process.stdout,
|
|
25
|
+
choices,
|
|
26
|
+
preSelectedChoices,
|
|
27
|
+
validators = []
|
|
28
|
+
} = options ?? {};
|
|
29
|
+
|
|
30
|
+
super(message, stdin, stdout);
|
|
31
|
+
|
|
32
|
+
if (!options) {
|
|
33
|
+
this.destroy();
|
|
34
|
+
throw new TypeError("Missing required options");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.options = options;
|
|
38
|
+
|
|
39
|
+
if (!choices?.length) {
|
|
40
|
+
this.destroy();
|
|
41
|
+
throw new TypeError("Missing required param: choices");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.longestChoice = Math.max(...choices.map((choice) => {
|
|
45
|
+
if (typeof choice === "string") {
|
|
46
|
+
return choice.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const kRequiredChoiceProperties = ["label", "value"];
|
|
50
|
+
|
|
51
|
+
for (const prop of kRequiredChoiceProperties) {
|
|
52
|
+
if (!choice[prop]) {
|
|
53
|
+
this.destroy();
|
|
54
|
+
throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return choice.label.length;
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
this.#validators = validators;
|
|
62
|
+
|
|
63
|
+
if (!preSelectedChoices) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const choice of preSelectedChoices) {
|
|
68
|
+
const choiceIndex = this.choices.findIndex((item) => {
|
|
69
|
+
if (typeof item === "string") {
|
|
70
|
+
return item === choice;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return item.value === choice;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (choiceIndex === -1) {
|
|
77
|
+
throw new Error(`Invalid pre-selected choice: ${choice.value ?? choice}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.selectedIndexes.push(choiceIndex);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#getFormattedChoice(choiceIndex) {
|
|
85
|
+
const choice = this.choices[choiceIndex];
|
|
86
|
+
|
|
87
|
+
if (typeof choice === "string") {
|
|
88
|
+
return { value: choice, label: choice };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return choice;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#getVisibleChoices() {
|
|
95
|
+
const maxVisible = this.options.maxVisible || 8;
|
|
96
|
+
let startIndex = Math.min(this.choices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
|
|
97
|
+
if (startIndex < 0) {
|
|
98
|
+
startIndex = 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const endIndex = Math.min(startIndex + maxVisible, this.choices.length);
|
|
102
|
+
|
|
103
|
+
return { startIndex, endIndex };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#showChoices() {
|
|
107
|
+
const { startIndex, endIndex } = this.#getVisibleChoices();
|
|
108
|
+
this.lastRender = { startIndex, endIndex };
|
|
109
|
+
|
|
110
|
+
for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
|
|
111
|
+
const choice = this.#getFormattedChoice(choiceIndex);
|
|
112
|
+
const isChoiceActive = choiceIndex === this.activeIndex;
|
|
113
|
+
const isChoiceSelected = this.selectedIndexes.includes(choiceIndex);
|
|
114
|
+
const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
|
|
115
|
+
const showNextChoicesArrow = endIndex < this.choices.length && choiceIndex === endIndex - 1;
|
|
116
|
+
|
|
117
|
+
let prefixArrow = " ";
|
|
118
|
+
if (showPreviousChoicesArrow) {
|
|
119
|
+
prefixArrow = SYMBOLS.Previous + " ";
|
|
120
|
+
}
|
|
121
|
+
else if (showNextChoicesArrow) {
|
|
122
|
+
prefixArrow = SYMBOLS.Next + " ";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const prefix = `${prefixArrow}${isChoiceSelected ? SYMBOLS.Active : SYMBOLS.Inactive}`;
|
|
126
|
+
const formattedLabel = choice.label.padEnd(
|
|
127
|
+
this.longestChoice < 10 ? this.longestChoice : 0
|
|
128
|
+
);
|
|
129
|
+
const formattedDescription = choice.description ? ` - ${choice.description}` : "";
|
|
130
|
+
const color = isChoiceActive ? kleur.white().bold : kleur.gray;
|
|
131
|
+
const str = `${prefix} ${color(`${formattedLabel}${formattedDescription}`)}${EOL}`;
|
|
132
|
+
|
|
133
|
+
this.write(str);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#showAnsweredQuestion(choices, isAgentAnswer = false) {
|
|
138
|
+
const prefixSymbol = this.selectedIndexes.length === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick;
|
|
139
|
+
const prefix = `${prefixSymbol} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`;
|
|
140
|
+
const formattedChoice = kleur.yellow(choices);
|
|
141
|
+
|
|
142
|
+
this.write(`${prefix}${choices ? ` ${formattedChoice}` : ""}${EOL}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async multiselect() {
|
|
146
|
+
if (this.agent.nextAnswers.length > 0) {
|
|
147
|
+
const answer = this.agent.nextAnswers.shift();
|
|
148
|
+
this.#showAnsweredQuestion(answer, true);
|
|
149
|
+
this.destroy();
|
|
150
|
+
|
|
151
|
+
return answer;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.write(SYMBOLS.HideCursor);
|
|
155
|
+
this.#showQuestion();
|
|
156
|
+
|
|
157
|
+
const render = (options = {}) => {
|
|
158
|
+
const {
|
|
159
|
+
initialRender = false,
|
|
160
|
+
clearRender = false,
|
|
161
|
+
error = null
|
|
162
|
+
} = options;
|
|
163
|
+
|
|
164
|
+
if (!initialRender) {
|
|
165
|
+
let linesToClear = this.lastRender.endIndex - this.lastRender.startIndex;
|
|
166
|
+
while (linesToClear > 0) {
|
|
167
|
+
this.clearLastLine();
|
|
168
|
+
linesToClear--;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (clearRender) {
|
|
173
|
+
this.stdout.moveCursor(0, -2);
|
|
174
|
+
this.stdout.clearScreenDown();
|
|
175
|
+
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (error) {
|
|
180
|
+
this.stdout.moveCursor(0, -2);
|
|
181
|
+
this.stdout.clearScreenDown();
|
|
182
|
+
this.#showQuestion(error);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.#showChoices();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
render({ initialRender: true });
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
const onKeypress = (value, key) => {
|
|
192
|
+
if (key.name === "up") {
|
|
193
|
+
this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
|
|
194
|
+
render();
|
|
195
|
+
}
|
|
196
|
+
else if (key.name === "down") {
|
|
197
|
+
this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
|
|
198
|
+
render();
|
|
199
|
+
}
|
|
200
|
+
else if (key.name === "a") {
|
|
201
|
+
this.selectedIndexes = this.selectedIndexes.length === this.choices.length ? [] : this.choices.map((_, index) => index);
|
|
202
|
+
render();
|
|
203
|
+
}
|
|
204
|
+
else if (key.name === "space") {
|
|
205
|
+
const isChoiceSelected = this.selectedIndexes.includes(this.activeIndex);
|
|
206
|
+
|
|
207
|
+
if (isChoiceSelected) {
|
|
208
|
+
this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.selectedIndexes.push(this.activeIndex);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
render();
|
|
215
|
+
}
|
|
216
|
+
else if (key.name === "return") {
|
|
217
|
+
const labels = this.selectedIndexes.map((index) => this.choices[index].label ?? this.choices[index]);
|
|
218
|
+
const values = this.selectedIndexes.map((index) => this.choices[index].value ?? this.choices[index]);
|
|
219
|
+
|
|
220
|
+
for (const validator of this.#validators) {
|
|
221
|
+
if (!validator.validate(values)) {
|
|
222
|
+
const error = validator.error(values);
|
|
223
|
+
render({ error });
|
|
224
|
+
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.stdin.off("keypress", onKeypress);
|
|
230
|
+
|
|
231
|
+
render({ clearRender: true });
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
this.#showAnsweredQuestion(labels.join(", "));
|
|
235
|
+
|
|
236
|
+
this.write(SYMBOLS.ShowCursor);
|
|
237
|
+
this.destroy();
|
|
238
|
+
|
|
239
|
+
resolve(values);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
this.stdin.on("keypress", onKeypress);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#showQuestion(error = null) {
|
|
248
|
+
let hint = kleur.gray(
|
|
249
|
+
`(Press ${kleur.bold("<a>")} to toggle all, ${kleur.bold("<space>")} to select, ${kleur.bold("<return>")} to submit)`
|
|
250
|
+
);
|
|
251
|
+
if (error) {
|
|
252
|
+
hint += ` ${kleur.red().bold(`[${error}]`)}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)} ${hint}${EOL}`);
|
|
256
|
+
}
|
|
257
|
+
}
|