@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 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, 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
47
42
 
48
- ### `prompt(message: string): Promise<string>`
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(message: string, options: { choices: (Choice | string)[], maxVisible?: number }): Promise<string>`
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(message: string, options?: { initial: boolean }): Promise<string>`
60
+ ### `confirm()`
57
61
 
58
- Boolean prompt.
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: { choices: (Choice | string)[], maxVisble?: number }): 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,167 +1,22 @@
1
- import { createInterface } from 'node:readline/promises'
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
- import ansi from 'ansi-styles'
6
+ export async function prompt(message) {
7
+ const textPrompt = new TextPrompt(message);
4
8
 
5
- const kQuestionMark = `${ansi.blue.open}?${ansi.blue.close}`
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 (message, options) {
40
- if (typeof message !== 'string') {
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
- let startIndex = Math.min(total - maxVisible, currentIndex - Math.floor(maxVisible / 2))
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
- const kDefaultConfirmOptions = { initial: false }
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 result
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.1",
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,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
- "standard": "^17.0.0"
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
+ }
@@ -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
+ }