@topcli/prompts 0.0.2 → 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 CHANGED
@@ -24,23 +24,18 @@ $ 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, select, confirm } from '@topcli/prompts'
29
-
30
- const name = await prompt('What\'s your name ?')
31
- const gender = await select('What\'s your gender ?', {
32
- choices: [
33
- {
34
- value: 'M',
35
- label: 'Male'
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
@@ -56,7 +51,7 @@ Simple prompt, similar to `rl.question()` with an improved UI.
56
51
  ### `select()`
57
52
 
58
53
  ```ts
59
- select(message: string, options: { choices: (Choice | string)[], maxVisible?: number, ignoreValues?: (string | number | boolean)[] }): Promise<string>
54
+ select(message: string, options: SelectOptions): Promise<string>
60
55
  ```
61
56
 
62
57
  Scrollable select depending `maxVisible` (default `8`).
@@ -65,7 +60,7 @@ Use `ignoreValues` to skip result render & clear lines after a selected one.
65
60
  ### `confirm()`
66
61
 
67
62
  ```ts
68
- confirm(message: string, options?: { initial: boolean }): Promise<string>
63
+ confirm(message: string, options?: ConfirmOptions): Promise<string>
69
64
  ```
70
65
 
71
66
  Boolean prompt, return `options.initial` if user input is different from "y"/"yes"/"n"/"no", (default `false`).
@@ -74,8 +69,22 @@ Boolean prompt, return `options.initial` if user input is different from "y"/"ye
74
69
 
75
70
  ```ts
76
71
  export interface Choice {
77
- value: any,
78
- label: string,
79
- 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;
80
89
  }
81
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: { choices: (Choice | string)[], maxVisble?: number, ignoreValues: (string | number | boolean)[] }): Promise<string>;
8
- export function confirm(message: string, options?: { initial: boolean }): Promise<string>;
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,170 +1,22 @@
1
- import { createInterface } from 'node:readline'
2
- import { promisify } from 'node:util'
3
- import { EOL } from 'node:os'
4
- import ansi from 'ansi-styles'
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";
5
5
 
6
- const kQuestionMark = `${ansi.blue.open}?${ansi.blue.close}`
7
- const kTick = `${ansi.green.open}✔${ansi.green.close}`
8
- const kCross = `${ansi.red.open}✖${ansi.red.close}`
9
- const kPointer = `${ansi.gray.open}›${ansi.gray.close}`
10
- const kActive = `${ansi.reset.open}${kPointer} `
11
- const kInactive = `${ansi.gray.open} `
12
- const kPrevious = '⭡'
13
- const kNext = '⭣'
14
- const kShowCursor = '\x1B[?25h'
15
- const kHideCursor = '\x1B[?25l'
6
+ export async function prompt(message) {
7
+ const textPrompt = new TextPrompt(message);
16
8
 
17
- function clearLastLine (stdout = process.stdout) {
18
- stdout.moveCursor(0, -1)
19
- stdout.clearLine()
9
+ return textPrompt.question();
20
10
  }
21
11
 
22
- export async function prompt (message) {
23
- if (typeof message !== 'string') {
24
- throw new TypeError('message must be a string')
25
- }
12
+ export async function select(message, options) {
13
+ const selectPrompt = new SelectPrompt(message, options);
26
14
 
27
- const { stdin: input, stdout: output } = process
28
- const rl = createInterface({ input, output })
29
-
30
- const answer = await promisify(rl.question)(`${ansi.bold.open}${kQuestionMark} ${message}${ansi.bold.close} `)
31
-
32
- clearLastLine()
33
- console.log(`${ansi.bold.open}${answer ? kTick : kCross} ${message} ${kPointer} ${ansi.yellow.open}${answer}${ansi.yellow.close}${ansi.bold.close}`)
34
-
35
- rl.close()
36
-
37
- return answer
38
- }
39
-
40
- export async function select (message, options) {
41
- if (typeof message !== 'string') {
42
- throw new TypeError('message must be a string')
43
- }
44
-
45
- if (!options) {
46
- throw new TypeError('Missing required options')
47
- }
48
- const { choices, ignoreValues, stdout = process.stdout, stdin = process.stdin } = 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
- stdout.write(kHideCursor)
71
- const rl = options.stdin && options.stdout ? null : createInterface({ input: stdin, output: stdout })
72
-
73
- let activeIndex = 0
74
-
75
- stdout.write(`${ansi.bold.open}${kQuestionMark} ${message}${ansi.bold.close}${EOL}`)
76
-
77
- let lastRender = null
78
- const render = (initialRender = false, { reset } = {}) => {
79
- const getVisibleChoices = (currentIndex, total, maxVisible) => {
80
- maxVisible = maxVisible || total
81
-
82
- let startIndex = Math.min(total - maxVisible, currentIndex - Math.floor(maxVisible / 2))
83
- if (startIndex < 0) {
84
- startIndex = 0
85
- }
86
-
87
- const endIndex = Math.min(startIndex + maxVisible, total)
88
-
89
- return { startIndex, endIndex }
90
- }
91
-
92
- const { startIndex, endIndex } = getVisibleChoices(activeIndex, choices.length, options.maxVisible || 8)
93
-
94
- if (!initialRender) {
95
- const linesToClear = lastRender.endIndex - lastRender.startIndex
96
- stdout.moveCursor(0, -linesToClear)
97
- stdout.clearScreenDown()
98
- }
99
-
100
- if (reset) {
101
- clearLastLine(stdout)
102
- clearLastLine(stdout)
103
-
104
- return
105
- }
106
-
107
- lastRender = { startIndex, endIndex }
108
-
109
- for (let i = startIndex; i < endIndex; i++) {
110
- const choice = typeof choices[i] === 'string' ? { value: choices[i], label: choices[i] } : choices[i]
111
- const prefix = `${startIndex > 0 && i === startIndex ? kPrevious : endIndex < choices.length && i === endIndex - 1 ? kNext : ' '}${i === activeIndex ? kActive : kInactive}`
112
- const str = `${prefix}${choice.label.padEnd(longestChoice < 10 ? longestChoice : 0)}${choice.description ? ` - ${choice.description}` : ''}${ansi.reset.open}${EOL}`
113
- stdout.write(str)
114
- }
115
- }
116
-
117
- render(true)
118
-
119
- return new Promise((resolve) => {
120
- const onKeypress = (value, key) => {
121
- if (key.name === 'up') {
122
- activeIndex = activeIndex === 0 ? choices.length - 1 : activeIndex - 1
123
- render()
124
- } else if (key.name === 'down') {
125
- activeIndex = activeIndex === choices.length - 1 ? 0 : activeIndex + 1
126
- render()
127
- } else if (key.name === 'return') {
128
- stdin.off('keypress', onKeypress)
129
-
130
- render(false, { reset: true })
131
-
132
- const value = choices[activeIndex].value ?? choices[activeIndex]
133
- if (!ignoreValues?.includes(value)) {
134
- stdout.write(`${ansi.bold.open}${kTick} ${message} ${kPointer} ${ansi.yellow.open}${choices[activeIndex].label ?? choices[activeIndex]}${ansi.reset.open}${EOL}`)
135
- }
136
-
137
- stdout.write(kShowCursor)
138
- rl?.close()
139
-
140
- resolve(value)
141
- }
142
- }
143
-
144
- stdin.on('keypress', onKeypress)
145
- })
15
+ return selectPrompt.select();
146
16
  }
147
17
 
148
- const kDefaultConfirmOptions = { initial: false }
149
-
150
- export async function confirm (message, options = kDefaultConfirmOptions) {
151
- if (typeof message !== 'string') {
152
- throw new TypeError('message must be a string')
153
- }
154
-
155
- const { initial } = { kDefaultConfirmOptions, ...options }
156
-
157
- const { stdin: input, stdout: output } = process
158
- const rl = createInterface({ input, output })
159
-
160
- const tip = initial ? `${ansi.bold.open}Yes${ansi.bold.close}/no` : `yes/${ansi.bold.open}No${ansi.bold.close}`
161
- const answer = await rl.question(`${kQuestionMark} ${message} ${ansi.grey.open}(${tip})${ansi.grey.close} ${kPointer} `)
162
- const result = answer ? ['y', 'yes'].includes(answer.toLocaleLowerCase()) : initial
163
-
164
- clearLastLine()
165
- console.log(`${result ? kTick : kCross} ${message}`)
166
-
167
- rl.close()
18
+ export async function confirm(message, options) {
19
+ const confirmPrompt = new ConfirmPrompt(message, options);
168
20
 
169
- return result
21
+ return confirmPrompt.confirm();
170
22
  }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@topcli/prompts",
3
- "version": "0.0.2",
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
- "lint": "standard --fix | snazzy"
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,18 @@
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
- "standard": "^17.0.0",
25
31
  "strip-ansi": "^7.0.1"
26
32
  },
27
33
  "dependencies": {
@@ -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
+ }
@@ -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
+ }