@topcli/prompts 0.0.1 → 0.0.3
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 +45 -23
- package/index.d.ts +16 -5
- package/index.js +13 -158
- package/package.json +13 -9
- package/src/abstract-prompt.js +32 -0
- package/src/confirm-prompt.js +62 -0
- package/src/constants.js +17 -0
- package/src/select-prompt.js +136 -0
- package/src/text-prompt.js +43 -0
package/README.md
CHANGED
|
@@ -24,45 +24,67 @@ $ yarn add @topcli/prompts
|
|
|
24
24
|
|
|
25
25
|
## Usage exemple
|
|
26
26
|
|
|
27
|
+
You can locally run `node ./demo.js`
|
|
28
|
+
|
|
27
29
|
```js
|
|
28
|
-
import { prompt,
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
value: 'F',
|
|
39
|
-
label: 'Female'
|
|
40
|
-
},
|
|
41
|
-
]
|
|
42
|
-
})
|
|
43
|
-
const isAdult = await confirm('Are you over 18 ?', { initial: true })
|
|
30
|
+
import { prompt, confirm, select } from '@topcli/prompts'
|
|
31
|
+
|
|
32
|
+
const kTestRunner = ['node', 'tap', 'tape', 'vitest', 'mocha', 'ava']
|
|
33
|
+
|
|
34
|
+
const name = await prompt('Project name ?')
|
|
35
|
+
const runner = await select('Choose a test runner', { choices: kTestRunner, maxVisible: 5 })
|
|
36
|
+
const isCLI = await confirm('Your project is a CLI ?', { initial: true })
|
|
37
|
+
|
|
38
|
+
console.log(name, runner, isCLI)
|
|
44
39
|
```
|
|
45
40
|
|
|
46
41
|
## API
|
|
47
42
|
|
|
48
|
-
### `prompt(
|
|
43
|
+
### `prompt()`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
prompt(message: string): Promise<string>
|
|
47
|
+
```
|
|
49
48
|
|
|
50
49
|
Simple prompt, similar to `rl.question()` with an improved UI.
|
|
51
50
|
|
|
52
|
-
### `select(
|
|
51
|
+
### `select()`
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
select(message: string, options: SelectOptions): Promise<string>
|
|
55
|
+
```
|
|
53
56
|
|
|
54
57
|
Scrollable select depending `maxVisible` (default `8`).
|
|
58
|
+
Use `ignoreValues` to skip result render & clear lines after a selected one.
|
|
55
59
|
|
|
56
|
-
### `confirm(
|
|
60
|
+
### `confirm()`
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
```ts
|
|
63
|
+
confirm(message: string, options?: ConfirmOptions): Promise<string>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Boolean prompt, return `options.initial` if user input is different from "y"/"yes"/"n"/"no", (default `false`).
|
|
59
67
|
|
|
60
68
|
## Interfaces
|
|
61
69
|
|
|
62
70
|
```ts
|
|
63
71
|
export interface Choice {
|
|
64
|
-
value: any
|
|
65
|
-
label: string
|
|
66
|
-
description?: string
|
|
72
|
+
value: any;
|
|
73
|
+
label: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export interface SelectOptions {
|
|
80
|
+
choices: (Choice | string)[];
|
|
81
|
+
maxVisible?: number = 8;
|
|
82
|
+
ignoreValues?: (string | number | boolean)[];
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export interface ConfirmOptions {
|
|
88
|
+
initial?: boolean = false;
|
|
67
89
|
}
|
|
68
90
|
```
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
export interface Choice {
|
|
2
|
-
value: any
|
|
3
|
-
label: string
|
|
4
|
-
description?: string
|
|
2
|
+
value: any;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
5
|
}
|
|
6
|
+
|
|
7
|
+
export interface SelectOptions {
|
|
8
|
+
choices: (Choice | string)[];
|
|
9
|
+
maxVisible?: number;
|
|
10
|
+
ignoreValues?: (string | number | boolean)[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ConfirmOptions {
|
|
14
|
+
initial?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
export function prompt(message: string): Promise<string>;
|
|
7
|
-
export function select(message: string, options:
|
|
8
|
-
export function confirm(message: string, options?:
|
|
18
|
+
export function select(message: string, options: SelectOptions): Promise<string>;
|
|
19
|
+
export function confirm(message: string, options?: ConfirmOptions): Promise<string>;
|
package/index.js
CHANGED
|
@@ -1,167 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
// Import Internal Dependencies
|
|
2
|
+
import { ConfirmPrompt } from "./src/confirm-prompt.js";
|
|
3
|
+
import { SelectPrompt } from "./src/select-prompt.js";
|
|
4
|
+
import { TextPrompt } from "./src/text-prompt.js";
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
export async function prompt(message) {
|
|
7
|
+
const textPrompt = new TextPrompt(message);
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
const kTick = `${ansi.green.open}✔${ansi.green.close}`
|
|
7
|
-
const kCross = `${ansi.red.open}✖${ansi.red.close}`
|
|
8
|
-
const kPointer = `${ansi.gray.open}›${ansi.gray.close}`
|
|
9
|
-
const kActive = `${ansi.reset.open}${kPointer} `
|
|
10
|
-
const kInactive = `${ansi.gray.open} `
|
|
11
|
-
const kPrevious = '⭡'
|
|
12
|
-
const kNext = '⭣'
|
|
13
|
-
const kShowCursor = '\x1B[?25h'
|
|
14
|
-
const kHideCursor = '\x1B[?25l'
|
|
15
|
-
|
|
16
|
-
function clearLastLine () {
|
|
17
|
-
process.stdout.moveCursor(0, -1)
|
|
18
|
-
process.stdout.clearLine()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function prompt (message) {
|
|
22
|
-
if (typeof message !== 'string') {
|
|
23
|
-
throw new TypeError('message must be a string')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { stdin: input, stdout: output } = process
|
|
27
|
-
const rl = createInterface({ input, output })
|
|
28
|
-
|
|
29
|
-
const answer = await rl.question(`${ansi.bold.open} ${kPointer} ${message} ${ansi.bold.close}`)
|
|
30
|
-
|
|
31
|
-
clearLastLine()
|
|
32
|
-
console.log(`${ansi.bold.open}${answer ? kTick : kCross} ${message} ${kPointer} ${ansi.yellow.open}${answer}${ansi.yellow.close}${ansi.bold.close}`)
|
|
33
|
-
|
|
34
|
-
rl.close()
|
|
35
|
-
|
|
36
|
-
return answer
|
|
9
|
+
return textPrompt.question();
|
|
37
10
|
}
|
|
38
11
|
|
|
39
|
-
export async function select
|
|
40
|
-
|
|
41
|
-
throw new TypeError('message must be a string')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!options) {
|
|
45
|
-
throw new TypeError('Missing required options')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const { choices } = options
|
|
49
|
-
|
|
50
|
-
if (!choices?.length) {
|
|
51
|
-
throw new TypeError('Missing required param: choices')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const longestChoice = Math.max(...choices.map(choice => {
|
|
55
|
-
if (typeof choice === 'string') {
|
|
56
|
-
return choice.length
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const kRequiredChoiceProperties = ['label', 'value']
|
|
60
|
-
|
|
61
|
-
for (const prop of kRequiredChoiceProperties) {
|
|
62
|
-
if (!choice[prop]) {
|
|
63
|
-
throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return choice.label.length
|
|
68
|
-
}))
|
|
69
|
-
|
|
70
|
-
process.stdout.write(kHideCursor)
|
|
71
|
-
const { stdin: input, stdout: output } = process
|
|
72
|
-
const rl = createInterface({ input, output })
|
|
73
|
-
|
|
74
|
-
let activeIndex = 0
|
|
75
|
-
|
|
76
|
-
console.log(`${ansi.bold.open}${kQuestionMark} ${message}${ansi.bold.close}`)
|
|
77
|
-
|
|
78
|
-
let lastRender = null
|
|
79
|
-
const render = (initialRender = false, { reset } = {}) => {
|
|
80
|
-
const getVisibleChoices = (currentIndex, total, maxVisible) => {
|
|
81
|
-
maxVisible = maxVisible || total
|
|
12
|
+
export async function select(message, options) {
|
|
13
|
+
const selectPrompt = new SelectPrompt(message, options);
|
|
82
14
|
|
|
83
|
-
|
|
84
|
-
if (startIndex < 0) {
|
|
85
|
-
startIndex = 0
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const endIndex = Math.min(startIndex + maxVisible, total)
|
|
89
|
-
|
|
90
|
-
return { startIndex, endIndex }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const { startIndex, endIndex } = getVisibleChoices(activeIndex, choices.length, options.maxVisible || 8)
|
|
94
|
-
|
|
95
|
-
if (!initialRender) {
|
|
96
|
-
const linesToClear = lastRender.endIndex - lastRender.startIndex
|
|
97
|
-
process.stdout.clearLine(1)
|
|
98
|
-
process.stdout.moveCursor(0, -linesToClear)
|
|
99
|
-
process.stdout.clearLine(1)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (reset) {
|
|
103
|
-
clearLastLine()
|
|
104
|
-
clearLastLine()
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
lastRender = { startIndex, endIndex }
|
|
109
|
-
|
|
110
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
111
|
-
const choice = typeof choices[i] === 'string' ? { value: choices[i], label: choices[i] } : choices[i]
|
|
112
|
-
const prefix = `${startIndex > 0 && i === startIndex ? kPrevious : endIndex < choices.length && i === endIndex - 1 ? kNext : ' '}${i === activeIndex ? kActive : kInactive}`
|
|
113
|
-
const str = `${prefix}${choice.label.padEnd(longestChoice < 10 ? longestChoice : 0)}${choice.description ? ` - ${choice.description}` : ''}${ansi.reset.open}`
|
|
114
|
-
console.log(str)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
render(true)
|
|
119
|
-
|
|
120
|
-
return new Promise((resolve) => {
|
|
121
|
-
const onKeypress = (value, key) => {
|
|
122
|
-
if (key.name === 'up') {
|
|
123
|
-
activeIndex = activeIndex === 0 ? choices.length - 1 : activeIndex - 1
|
|
124
|
-
render()
|
|
125
|
-
} else if (key.name === 'down') {
|
|
126
|
-
activeIndex = activeIndex === choices.length - 1 ? 0 : activeIndex + 1
|
|
127
|
-
render()
|
|
128
|
-
} else if (key.name === 'return') {
|
|
129
|
-
process.stdin.off('keypress', onKeypress)
|
|
130
|
-
render(false, { reset: true })
|
|
131
|
-
|
|
132
|
-
console.log(`${ansi.bold.open}${kTick} ${message} ${kPointer} ${ansi.yellow.open}${choices[activeIndex].value}${ansi.reset.open}`)
|
|
133
|
-
|
|
134
|
-
process.stderr.write(kShowCursor)
|
|
135
|
-
rl.close()
|
|
136
|
-
|
|
137
|
-
resolve(choices[activeIndex].value)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
process.stdin.on('keypress', onKeypress)
|
|
142
|
-
})
|
|
15
|
+
return selectPrompt.select();
|
|
143
16
|
}
|
|
144
17
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
export async function confirm (message, options = kDefaultConfirmOptions) {
|
|
148
|
-
if (typeof message !== 'string') {
|
|
149
|
-
throw new TypeError('message must be a string')
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const { initial } = { kDefaultConfirmOptions, ...options }
|
|
153
|
-
|
|
154
|
-
const { stdin: input, stdout: output } = process
|
|
155
|
-
const rl = createInterface({ input, output })
|
|
156
|
-
|
|
157
|
-
const tip = initial ? `${ansi.bold.open}Yes${ansi.bold.close}/no` : `yes/${ansi.bold.open}No${ansi.bold.close}`
|
|
158
|
-
const answer = await rl.question(`${kQuestionMark} ${message} ${ansi.grey.open}(${tip})${ansi.grey.close} ${kPointer} `)
|
|
159
|
-
const result = (!answer && initial) || ['y', 'yes'].includes(answer.toLocaleLowerCase())
|
|
160
|
-
|
|
161
|
-
clearLastLine()
|
|
162
|
-
console.log(`${result ? kTick : kCross} ${message}`)
|
|
163
|
-
|
|
164
|
-
rl.close()
|
|
18
|
+
export async function confirm(message, options) {
|
|
19
|
+
const confirmPrompt = new ConfirmPrompt(message, options);
|
|
165
20
|
|
|
166
|
-
return
|
|
21
|
+
return confirmPrompt.confirm();
|
|
167
22
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topcli/prompts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Node.js user input library for command-line interfaces.",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "node --test ./test/**.test.js",
|
|
7
|
-
"
|
|
6
|
+
"test": "node --loader=esmock --test ./test/**.test.js",
|
|
7
|
+
"coverage": "c8 -r html npm run test",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"lint:fix": "eslint . --fix"
|
|
8
10
|
},
|
|
9
11
|
"main": "./index.js",
|
|
10
12
|
"keywords": [
|
|
@@ -14,14 +16,19 @@
|
|
|
14
16
|
],
|
|
15
17
|
"files": [
|
|
16
18
|
"index.js",
|
|
17
|
-
"index.d.ts"
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"src"
|
|
18
21
|
],
|
|
19
22
|
"author": "PierreDemailly <pierredemailly.pro@gmail.com>",
|
|
20
23
|
"license": "ISC",
|
|
21
24
|
"type": "module",
|
|
22
25
|
"devDependencies": {
|
|
26
|
+
"@nodesecure/eslint-config": "^1.7.0",
|
|
27
|
+
"c8": "^7.13.0",
|
|
28
|
+
"eslint": "^8.38.0",
|
|
29
|
+
"esmock": "^2.1.0",
|
|
23
30
|
"snazzy": "^9.0.0",
|
|
24
|
-
"
|
|
31
|
+
"strip-ansi": "^7.0.1"
|
|
25
32
|
},
|
|
26
33
|
"dependencies": {
|
|
27
34
|
"ansi-styles": "^6.2.1"
|
|
@@ -32,8 +39,5 @@
|
|
|
32
39
|
"bugs": {
|
|
33
40
|
"url": "https://github.com/TopCli/prompts/issues"
|
|
34
41
|
},
|
|
35
|
-
"homepage": "https://github.com/TopCli/prompts#readme"
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
38
|
-
}
|
|
42
|
+
"homepage": "https://github.com/TopCli/prompts#readme"
|
|
39
43
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
export class AbstractPrompt {
|
|
5
|
+
constructor(message, input = process.stdin, output = process.stdout) {
|
|
6
|
+
if (this.constructor === AbstractPrompt) {
|
|
7
|
+
throw new Error("AbstractPrompt can't be instantiated.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof message !== "string") {
|
|
11
|
+
throw new TypeError(`message must be string, ${typeof message} given.`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this.stdin = input;
|
|
15
|
+
this.stdout = output;
|
|
16
|
+
this.message = message;
|
|
17
|
+
|
|
18
|
+
if (this.stdout.isTTY) {
|
|
19
|
+
this.stdin.setRawMode(true);
|
|
20
|
+
}
|
|
21
|
+
this.rl = createInterface({ input, output });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clearLastLine() {
|
|
25
|
+
this.stdout.moveCursor(0, -1);
|
|
26
|
+
this.stdout.clearLine();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
destroy() {
|
|
30
|
+
this.rl.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
|
|
4
|
+
// Import Third-party Dependencies
|
|
5
|
+
import ansi from "ansi-styles";
|
|
6
|
+
|
|
7
|
+
// Import Internal Dependencies
|
|
8
|
+
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
9
|
+
import { SYMBOLS } from "./constants.js";
|
|
10
|
+
|
|
11
|
+
const kDefaultConfirmOptions = { initial: false };
|
|
12
|
+
|
|
13
|
+
export class ConfirmPrompt extends AbstractPrompt {
|
|
14
|
+
#question;
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line max-params
|
|
17
|
+
constructor(message, options = {}, stdin = process.stdin, stdout = process.stdout) {
|
|
18
|
+
super(message, stdin, stdout);
|
|
19
|
+
|
|
20
|
+
const { initial } = { ...kDefaultConfirmOptions, ...options };
|
|
21
|
+
const Yes = `${ansi.bold.open}Yes${ansi.bold.close}`;
|
|
22
|
+
const No = `${ansi.bold.open}No${ansi.bold.close}`;
|
|
23
|
+
const tip = initial ? `${Yes}/no` : `yes/${No}`;
|
|
24
|
+
|
|
25
|
+
this.initial = initial;
|
|
26
|
+
this.#question = new Promise((resolve) => {
|
|
27
|
+
const question = `\
|
|
28
|
+
${ansi.bold.open}${SYMBOLS.QuestionMark} ${message}${ansi.bold.close} ${ansi.grey.open}(${tip})${ansi.grey.close} `;
|
|
29
|
+
this.rl.question.bind(this.rl)(question,
|
|
30
|
+
(answer) => {
|
|
31
|
+
resolve(answer);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#onQuestionAnswer() {
|
|
38
|
+
this.clearLastLine();
|
|
39
|
+
this.stdout.write(`${this.answer ? SYMBOLS.Tick : SYMBOLS.Cross} ${this.message}${EOL}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#validateResult(result) {
|
|
43
|
+
if (typeof result !== "string") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return ["y", "yes", "no", "n"].includes(result.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async confirm() {
|
|
51
|
+
try {
|
|
52
|
+
const result = await this.#question;
|
|
53
|
+
this.answer = this.#validateResult(result) ? ["y", "yes"].includes(result.toLocaleLowerCase()) : this.initial;
|
|
54
|
+
this.#onQuestionAnswer();
|
|
55
|
+
|
|
56
|
+
return this.answer;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
this.destroy();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Import Third-party Dependencies
|
|
2
|
+
import ansi from "ansi-styles";
|
|
3
|
+
|
|
4
|
+
const kPointer = `${ansi.gray.open}›${ansi.gray.close}`;
|
|
5
|
+
|
|
6
|
+
export const SYMBOLS = {
|
|
7
|
+
QuestionMark: `${ansi.blue.open}?${ansi.blue.close}`,
|
|
8
|
+
Tick: `${ansi.green.open}✔${ansi.green.close}`,
|
|
9
|
+
Cross: `${ansi.red.open}✖${ansi.red.close}`,
|
|
10
|
+
Pointer: kPointer,
|
|
11
|
+
Active: `${ansi.reset.open}${kPointer}`,
|
|
12
|
+
Inactive: `${ansi.gray.open}`,
|
|
13
|
+
Previous: "⭡",
|
|
14
|
+
Next: "⭣",
|
|
15
|
+
ShowCursor: "\x1B[?25h",
|
|
16
|
+
HideCursor: "\x1B[?25l"
|
|
17
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
|
|
4
|
+
// Import Third-party Dependencies
|
|
5
|
+
import ansi from "ansi-styles";
|
|
6
|
+
|
|
7
|
+
// Import Internal Dependencies
|
|
8
|
+
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
9
|
+
import { SYMBOLS } from "./constants.js";
|
|
10
|
+
|
|
11
|
+
export class SelectPrompt extends AbstractPrompt {
|
|
12
|
+
activeIndex = 0;
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line max-params
|
|
15
|
+
constructor(message, options, stdin = process.stdin, stdout = process.stdout) {
|
|
16
|
+
super(message, stdin, stdout);
|
|
17
|
+
|
|
18
|
+
if (!options) {
|
|
19
|
+
this.destroy();
|
|
20
|
+
throw new TypeError("Missing required options");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.options = options;
|
|
24
|
+
const { choices } = options;
|
|
25
|
+
|
|
26
|
+
if (!choices?.length) {
|
|
27
|
+
this.destroy();
|
|
28
|
+
throw new TypeError("Missing required param: choices");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.choices = choices;
|
|
32
|
+
this.longestChoice = Math.max(...choices.map((choice) => {
|
|
33
|
+
if (typeof choice === "string") {
|
|
34
|
+
return choice.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const kRequiredChoiceProperties = ["label", "value"];
|
|
38
|
+
|
|
39
|
+
for (const prop of kRequiredChoiceProperties) {
|
|
40
|
+
if (!choice[prop]) {
|
|
41
|
+
this.destroy();
|
|
42
|
+
throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return choice.label.length;
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async select() {
|
|
51
|
+
this.stdout.write(SYMBOLS.HideCursor);
|
|
52
|
+
this.stdout.write(`${ansi.bold.open}${SYMBOLS.QuestionMark} ${this.message}${ansi.bold.close}${EOL}`);
|
|
53
|
+
|
|
54
|
+
let lastRender = null;
|
|
55
|
+
const render = (initialRender = false, { reset } = {}) => {
|
|
56
|
+
function getVisibleChoices(currentIndex, total, maxVisible) {
|
|
57
|
+
let startIndex = Math.min(total - maxVisible, currentIndex - Math.floor(maxVisible / 2));
|
|
58
|
+
if (startIndex < 0) {
|
|
59
|
+
startIndex = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
63
|
+
|
|
64
|
+
return { startIndex, endIndex };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { startIndex, endIndex } = getVisibleChoices(this.activeIndex, this.choices.length, this.options.maxVisible || 8);
|
|
68
|
+
|
|
69
|
+
if (!initialRender) {
|
|
70
|
+
const linesToClear = lastRender.endIndex - lastRender.startIndex;
|
|
71
|
+
this.stdout.moveCursor(0, -linesToClear);
|
|
72
|
+
this.stdout.clearScreenDown();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (reset) {
|
|
76
|
+
this.clearLastLine(this.stdout);
|
|
77
|
+
this.clearLastLine(this.stdout);
|
|
78
|
+
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lastRender = { startIndex, endIndex };
|
|
83
|
+
|
|
84
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
85
|
+
const choice = typeof this.choices[i] === "string" ? { value: this.choices[i], label: this.choices[i] } : this.choices[i];
|
|
86
|
+
const showPreviousChoicesArrow = startIndex > 0 && i === startIndex;
|
|
87
|
+
const showNextChoicesArrow = endIndex < this.choices.length && i === endIndex - 1;
|
|
88
|
+
let prefixArrow = " ";
|
|
89
|
+
if (showPreviousChoicesArrow) {
|
|
90
|
+
prefixArrow = SYMBOLS.Previous;
|
|
91
|
+
}
|
|
92
|
+
else if (showNextChoicesArrow) {
|
|
93
|
+
prefixArrow = SYMBOLS.Next;
|
|
94
|
+
}
|
|
95
|
+
const prefix = `${prefixArrow}${i === this.activeIndex ? `${SYMBOLS.Active} ` : `${SYMBOLS.Inactive} `}`;
|
|
96
|
+
const str = `${prefix}${choice.label.padEnd(
|
|
97
|
+
this.longestChoice < 10 ? this.longestChoice : 0
|
|
98
|
+
)}${choice.description ? ` - ${choice.description}` : ""}${ansi.reset.open}${EOL}`;
|
|
99
|
+
this.stdout.write(str);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
render(true);
|
|
104
|
+
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const onKeypress = (value, key) => {
|
|
107
|
+
if (key.name === "up") {
|
|
108
|
+
this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
|
|
109
|
+
render();
|
|
110
|
+
}
|
|
111
|
+
else if (key.name === "down") {
|
|
112
|
+
this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
|
|
113
|
+
render();
|
|
114
|
+
}
|
|
115
|
+
else if (key.name === "return") {
|
|
116
|
+
this.stdin.off("keypress", onKeypress);
|
|
117
|
+
|
|
118
|
+
render(false, { reset: true });
|
|
119
|
+
const currentChoice = this.choices[this.activeIndex];
|
|
120
|
+
const value = currentChoice.value ?? currentChoice;
|
|
121
|
+
if (!this.options.ignoreValues?.includes(value)) {
|
|
122
|
+
const prefix = `${ansi.bold.open}${SYMBOLS.Tick} ${this.message} ${SYMBOLS.Pointer}`;
|
|
123
|
+
const choice = `${ansi.yellow.open}${currentChoice.label ?? currentChoice}${ansi.reset.open}`;
|
|
124
|
+
this.stdout.write(`${prefix} ${choice}${EOL}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.stdout.write(SYMBOLS.ShowCursor);
|
|
128
|
+
this.destroy();
|
|
129
|
+
resolve(value);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
this.stdin.on("keypress", onKeypress);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import { EOL } from "node:os";
|
|
3
|
+
|
|
4
|
+
// Import Third-party Dependencies
|
|
5
|
+
import ansi from "ansi-styles";
|
|
6
|
+
|
|
7
|
+
// Import Internal Dependencies
|
|
8
|
+
import { AbstractPrompt } from "./abstract-prompt.js";
|
|
9
|
+
import { SYMBOLS } from "./constants.js";
|
|
10
|
+
|
|
11
|
+
export class TextPrompt extends AbstractPrompt {
|
|
12
|
+
#question;
|
|
13
|
+
|
|
14
|
+
constructor(message, stdin = process.stdin, stdout = process.stdout) {
|
|
15
|
+
super(message, stdin, stdout);
|
|
16
|
+
|
|
17
|
+
this.#question = new Promise((resolve) => {
|
|
18
|
+
this.rl.question(`${ansi.bold.open}${SYMBOLS.QuestionMark} ${message}${ansi.bold.close} `, (answer) => {
|
|
19
|
+
resolve(answer);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#onQuestionAnswer() {
|
|
25
|
+
this.clearLastLine();
|
|
26
|
+
const prefix = `${ansi.bold.open}${this.answer ? SYMBOLS.Tick : SYMBOLS.Cross}`;
|
|
27
|
+
const suffix = `${ansi.yellow.close}${ansi.bold.close}${EOL}`;
|
|
28
|
+
this.stdout.write(`${prefix} ${this.message} ${SYMBOLS.Pointer} ${ansi.yellow.open}${this.answer ?? ""}${suffix}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async question() {
|
|
32
|
+
try {
|
|
33
|
+
this.answer = await this.#question;
|
|
34
|
+
|
|
35
|
+
this.#onQuestionAnswer();
|
|
36
|
+
|
|
37
|
+
return this.answer;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
this.destroy();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|