@topcli/prompts 1.0.1 → 1.2.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 CHANGED
@@ -27,20 +27,15 @@ $ yarn add @topcli/prompts
27
27
  You can locally run `node ./demo.js`
28
28
 
29
29
  ```js
30
- import { prompt, confirm, select } from '@topcli/prompts';
30
+ import { question, confirm, select } from "@topcli/prompts";
31
31
 
32
- const kTestRunner = ['node', 'tap', 'tape', 'vitest', 'mocha', 'ava'];
32
+ const kTestRunner = ["node", "tap", "tape", "vitest", "mocha", "ava"];
33
33
 
34
- const name = await prompt('Project name ?');
35
- const runner = await select('Choose a test runner', {
36
- choices: kTestRunner,
37
- maxVisible: 5
38
- });
39
- const isCLI = await confirm('Your project is a CLI ?', {
40
- initial: true
41
- });
34
+ const name = await question("Project name ?", { defaultValue: "foo" });
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 });
42
37
 
43
- console.log(name, runner, isCLI);^
38
+ console.log(name, runner, isCLI);
44
39
  ```
45
40
 
46
41
  ## API
@@ -100,6 +95,7 @@ Boolean prompt, return `options.initial` if user input is different from "y"/"ye
100
95
 
101
96
  ```ts
102
97
  export interface PromptOptions {
98
+ defaultValue?: string;
103
99
  validators?: {
104
100
  validate: (input: string) => boolean;
105
101
  error: (input: string) => string;
@@ -140,6 +136,7 @@ export interface ConfirmOptions {
140
136
  <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/thomas-gentilhomme/"><img src="https://avatars.githubusercontent.com/u/4438263?v=4?s=100" width="100px;" alt="Gentilhomme"/><br /><sub><b>Gentilhomme</b></sub></a><br /><a href="https://github.com/TopCli/prompts/pulls?q=is%3Apr+reviewed-by%3Afraxken" title="Reviewed Pull Requests">👀</a></td>
141
137
  <td align="center" valign="top" width="14.28%"><a href="http://tonygo.dev"><img src="https://avatars0.githubusercontent.com/u/22824417?v=4?s=100" width="100px;" alt="Tony Gorez"/><br /><sub><b>Tony Gorez</b></sub></a><br /><a href="https://github.com/TopCli/prompts/pulls?q=is%3Apr+reviewed-by%3Atony-go" title="Reviewed Pull Requests">👀</a></td>
142
138
  <td align="center" valign="top" width="14.28%"><a href="http://sofiand.github.io/portfolio-client/"><img src="https://avatars.githubusercontent.com/u/39944043?v=4?s=100" width="100px;" alt="Yefis"/><br /><sub><b>Yefis</b></sub></a><br /><a href="https://github.com/TopCli/prompts/commits?author=SofianD" title="Code">💻</a> <a href="https://github.com/TopCli/prompts/commits?author=SofianD" title="Documentation">📖</a></td>
139
+ <td align="center" valign="top" width="14.28%"><a href="http://justie.dev"><img src="https://avatars.githubusercontent.com/u/7118300?v=4?s=100" width="100px;" alt="Ben"/><br /><sub><b>Ben</b></sub></a><br /><a href="https://github.com/TopCli/prompts/commits?author=JUSTIVE" title="Documentation">📖</a> <a href="#maintenance-JUSTIVE" title="Maintenance">🚧</a></td>
143
140
  </tr>
144
141
  </tbody>
145
142
  </table>
package/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
- export interface PromptOptions {
1
+ export interface QuestionOptions {
2
+ stdin?: NodeJS.ReadStream & {
3
+ fd: 0;
4
+ };
5
+ stdout?: NodeJS.WriteStream & {
6
+ fd: 1;
7
+ };
8
+ defaultValue?: string;
2
9
  validators?: Validator[];
3
10
  }
4
11
 
@@ -23,7 +30,7 @@ export interface ConfirmOptions {
23
30
  initial?: boolean;
24
31
  }
25
32
 
26
- export function question(message: string, options?: PromptOptions): Promise<string>;
33
+ export function question(message: string, options?: QuestionOptions): Promise<string>;
27
34
  export function select(message: string, options: SelectOptions): Promise<string>;
28
35
  export function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
29
36
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@topcli/prompts",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Node.js user input library for command-line interfaces.",
5
5
  "scripts": {
6
- "test": "node --loader=esmock --test ./test/**.test.js",
6
+ "test": "node --no-warnings=ExperimentalWarning --loader=esmock --test test/",
7
7
  "coverage": "c8 -r html npm run test",
8
8
  "lint": "eslint .",
9
9
  "lint:fix": "eslint . --fix"
@@ -24,13 +24,15 @@
24
24
  "type": "module",
25
25
  "devDependencies": {
26
26
  "@nodesecure/eslint-config": "^1.7.0",
27
- "c8": "^7.13.0",
28
- "eslint": "^8.38.0",
29
- "esmock": "^2.1.0"
27
+ "@types/node": "^20.4.5",
28
+ "c8": "^8.0.0",
29
+ "eslint": "^8.44.0",
30
+ "esmock": "^2.3.1"
30
31
  },
31
32
  "dependencies": {
32
- "ansi-styles": "^6.2.1",
33
- "strip-ansi": "^7.0.1"
33
+ "is-unicode-supported": "^1.3.0",
34
+ "kleur": "^4.1.5",
35
+ "strip-ansi": "^7.1.0"
34
36
  },
35
37
  "engines": {
36
38
  "node": ">=14"
@@ -2,13 +2,31 @@
2
2
  import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
- import ansi from "ansi-styles";
5
+ import kleur from "kleur";
6
6
 
7
7
  // Import Internal Dependencies
8
8
  import { AbstractPrompt } from "./abstract-prompt.js";
9
9
  import { SYMBOLS } from "./constants.js";
10
10
 
11
+ // CONSTANTS
12
+ const kToggleKeys = new Set([
13
+ "left",
14
+ "right",
15
+ "tab",
16
+ "q",
17
+ "a",
18
+ "d",
19
+ "h",
20
+ "j",
21
+ "k",
22
+ "l",
23
+ "space"
24
+ ]);
25
+
11
26
  export class ConfirmPrompt extends AbstractPrompt {
27
+ #boundKeyPressEvent;
28
+ #boundExitEvent;
29
+
12
30
  constructor(message, options = {}) {
13
31
  const {
14
32
  stdin = process.stdin,
@@ -17,55 +35,83 @@ export class ConfirmPrompt extends AbstractPrompt {
17
35
  } = options;
18
36
  super(message, stdin, stdout);
19
37
 
20
- const Yes = `${ansi.bold.open}Yes${ansi.bold.close}`;
21
- const No = `${ansi.bold.open}No${ansi.bold.close}`;
22
- const tip = initial ? `${Yes}/no` : `yes/${No}`;
23
-
24
38
  this.initial = initial;
39
+ this.selectedValue = initial;
40
+ }
41
+
42
+ #getHint() {
43
+ const Yes = kleur.bold().underline().cyan("Yes");
44
+ const No = kleur.bold().underline().cyan("No");
45
+
46
+ return this.selectedValue ? `${Yes}/No` : `Yes/${No}`;
47
+ }
48
+
49
+ #render() {
50
+ this.write(this.#getQuestionQuery());
25
51
  }
26
52
 
27
53
  #question() {
28
54
  return new Promise((resolve) => {
29
55
  const questionQuery = this.#getQuestionQuery();
30
56
 
31
- this.rl.question(questionQuery,
32
- (answer) => {
33
- this.history.push(questionQuery + answer);
57
+ this.write(questionQuery);
58
+
59
+ this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve);
60
+ this.stdin.on("keypress", this.#boundKeyPressEvent);
34
61
 
35
- resolve(answer);
36
- }
37
- );
62
+ this.#boundExitEvent = this.#onProcessExit.bind(this);
63
+ process.once("exit", this.#boundExitEvent);
38
64
  });
39
65
  }
40
66
 
41
- #getQuestionQuery() {
42
- const query = `${ansi.bold.open}${SYMBOLS.QuestionMark} ${this.message}${ansi.bold.close}`;
67
+ #onKeypress(resolve, value, key) {
68
+ this.stdin.pause();
69
+ this.stdout.moveCursor(-this.#getQuestionQuery().length, 0);
70
+ this.stdout.clearScreenDown(() => this.stdin.resume());
71
+
72
+ if (key.name === "return") {
73
+ resolve(this.selectedValue);
74
+
75
+ return;
76
+ }
43
77
 
44
- return `${query} ${ansi.grey.open}(${this.initial ? "Yes/no" : "yes/No"})${ansi.grey.close} `;
78
+ if (kToggleKeys.has(key.name)) {
79
+ this.selectedValue = !this.selectedValue;
80
+ }
81
+
82
+ this.#render();
45
83
  }
46
84
 
47
- #onQuestionAnswer() {
48
- this.clearLastLine();
49
- this.write(`${this.answer ? SYMBOLS.Tick : SYMBOLS.Cross} ${this.message}${EOL}`);
85
+ #onProcessExit() {
86
+ this.stdin.off("keypress", this.#boundKeyPressEvent);
50
87
  }
51
88
 
52
- #validateResult(result) {
53
- if (typeof result !== "string") {
54
- return false;
55
- }
89
+ #getQuestionQuery() {
90
+ const query = kleur.bold(`${SYMBOLS.QuestionMark} ${this.message}`);
56
91
 
57
- return ["y", "yes", "no", "n"].includes(result.toLowerCase());
92
+ return `${query} ${this.#getHint()}`;
93
+ }
94
+
95
+ #onQuestionAnswer() {
96
+ this.clearLastLine();
97
+ this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${kleur.bold(this.message)}${EOL}`);
58
98
  }
59
99
 
60
100
  async confirm() {
101
+ this.write(SYMBOLS.HideCursor);
102
+
61
103
  try {
62
- const result = await this.#question();
63
- this.answer = this.#validateResult(result) ? ["y", "yes"].includes(result.toLocaleLowerCase()) : this.initial;
104
+ await this.#question();
64
105
  this.#onQuestionAnswer();
65
106
 
66
- return this.answer;
107
+ return this.selectedValue;
67
108
  }
68
109
  finally {
110
+ this.write(SYMBOLS.ShowCursor);
111
+
112
+ this.#onProcessExit();
113
+ process.off("exit", this.#boundExitEvent);
114
+
69
115
  this.destroy();
70
116
  }
71
117
  }
package/src/constants.js CHANGED
@@ -1,17 +1,31 @@
1
1
  // Import Third-party Dependencies
2
- import ansi from "ansi-styles";
2
+ import kleur from "kleur";
3
+ import isUnicodeSupported from "is-unicode-supported";
3
4
 
4
- const kPointer = `${ansi.gray.open}›${ansi.gray.close}`;
5
+ const kMainSymbols = {
6
+ tick: "✔",
7
+ cross: "✖",
8
+ pointer: "›",
9
+ previous: "⭡",
10
+ next: "⭣"
11
+ };
12
+ const kFallbackSymbols = {
13
+ tick: "√",
14
+ cross: "×",
15
+ pointer: ">",
16
+ previous: "↑",
17
+ next: "↓"
18
+ };
19
+ const kSymbols = isUnicodeSupported() ? kMainSymbols : kFallbackSymbols;
20
+ const kPointer = kleur.gray(kSymbols.pointer);
5
21
 
6
22
  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}`,
23
+ QuestionMark: kleur.blue().bold("?"),
24
+ Tick: kleur.green().bold(kSymbols.tick),
25
+ Cross: kleur.red().bold(kSymbols.cross),
10
26
  Pointer: kPointer,
11
- Active: `${ansi.reset.open}${kPointer}`,
12
- Inactive: `${ansi.gray.open}`,
13
- Previous: "⭡",
14
- Next: "⭣",
27
+ Previous: kSymbols.previous,
28
+ Next: kSymbols.next,
15
29
  ShowCursor: "\x1B[?25h",
16
30
  HideCursor: "\x1B[?25l"
17
31
  };
@@ -2,8 +2,7 @@
2
2
  import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
- import ansi from "ansi-styles";
6
- import stripAnsi from "strip-ansi";
5
+ import kleur from "kleur";
7
6
 
8
7
  // Import Internal Dependencies
9
8
  import { AbstractPrompt } from "./abstract-prompt.js";
@@ -13,12 +12,22 @@ export class QuestionPrompt extends AbstractPrompt {
13
12
  #validators;
14
13
 
15
14
  constructor(message, options = {}) {
16
- const { stdin = process.stdin, stdout = process.stdout, validators = [] } = options;
15
+ const {
16
+ stdin = process.stdin,
17
+ stdout = process.stdout,
18
+ defaultValue,
19
+ validators = []
20
+ } = options;
21
+
17
22
  super(message, stdin, stdout);
18
23
 
24
+ if (defaultValue && typeof defaultValue !== "string") {
25
+ throw new TypeError("defaultValue must be a string");
26
+ }
27
+
28
+ this.defaultValue = defaultValue;
29
+ this.tip = this.defaultValue ? ` (${this.defaultValue})` : "";
19
30
  this.#validators = validators;
20
- this.questionPrefix = `${ansi.bold.open}${SYMBOLS.QuestionMark} `;
21
- this.questionSuffix = `${ansi.bold.close} `;
22
31
  this.questionSuffixError = "";
23
32
  }
24
33
 
@@ -35,19 +44,19 @@ export class QuestionPrompt extends AbstractPrompt {
35
44
  }
36
45
 
37
46
  #getQuestionQuery() {
38
- return `${this.questionPrefix}${this.message}${this.questionSuffix}${this.questionSuffixError}`;
47
+ return `${kleur.bold(`${SYMBOLS.QuestionMark} ${this.message}${this.tip}`)} ${this.questionSuffixError}`;
39
48
  }
40
49
 
41
50
  #setQuestionSuffixError(error) {
42
- const suffix = `${ansi.red.open}[${error}]${ansi.red.close} `;
51
+ const suffix = kleur.red(`[${error}] `);
43
52
  this.questionSuffixError = suffix;
44
53
  }
45
54
 
46
55
  #writeAnswer() {
47
- const prefix = `${ansi.bold.open}${this.answer ? SYMBOLS.Tick : SYMBOLS.Cross}`;
48
- const suffix = `${ansi.yellow.close}${ansi.bold.close}${EOL}`;
56
+ const prefix = this.answer ? SYMBOLS.Tick : SYMBOLS.Cross;
57
+ const answer = kleur.yellow(this.answer ?? "");
49
58
 
50
- this.write(`${prefix} ${this.message} ${SYMBOLS.Pointer} ${ansi.yellow.open}${this.answer ?? ""}${suffix}`);
59
+ this.write(`${prefix} ${kleur.bold(this.message)} ${SYMBOLS.Pointer} ${answer}${EOL}`);
51
60
  }
52
61
 
53
62
  #onQuestionAnswer() {
@@ -69,6 +78,10 @@ export class QuestionPrompt extends AbstractPrompt {
69
78
  async question() {
70
79
  this.answer = await this.#question();
71
80
 
81
+ if (this.answer === "" && this.defaultValue) {
82
+ this.answer = this.defaultValue;
83
+ }
84
+
72
85
  this.#onQuestionAnswer();
73
86
 
74
87
  while (this.answer?.constructor.name === "Promise") {
@@ -2,7 +2,7 @@
2
2
  import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
- import ansi from "ansi-styles";
5
+ import kleur from "kleur";
6
6
 
7
7
  // Import Internal Dependencies
8
8
  import { AbstractPrompt } from "./abstract-prompt.js";
@@ -82,6 +82,7 @@ export class SelectPrompt extends AbstractPrompt {
82
82
 
83
83
  for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
84
84
  const choice = this.#getFormattedChoice(choiceIndex);
85
+ const isChoiceSelected = choiceIndex === this.activeIndex;
85
86
  const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
86
87
  const showNextChoicesArrow = endIndex < this.choices.length && choiceIndex === endIndex - 1;
87
88
 
@@ -93,20 +94,21 @@ export class SelectPrompt extends AbstractPrompt {
93
94
  prefixArrow = SYMBOLS.Next;
94
95
  }
95
96
 
96
- const prefix = `${prefixArrow}${choiceIndex === this.activeIndex ? `${SYMBOLS.Active} ` : `${SYMBOLS.Inactive} `}`;
97
+ const prefix = `${prefixArrow}${isChoiceSelected ? `${SYMBOLS.Pointer} ` : " "}`;
97
98
  const formattedLabel = choice.label.padEnd(
98
99
  this.longestChoice < 10 ? this.longestChoice : 0
99
100
  );
100
101
  const formattedDescription = choice.description ? ` - ${choice.description}` : "";
101
- const str = `${prefix}${formattedLabel}${formattedDescription}${ansi.reset.open}${EOL}`;
102
+ const color = isChoiceSelected ? kleur.white().bold : kleur.gray;
103
+ const str = color(`${prefix}${formattedLabel}${formattedDescription}${EOL}`);
102
104
 
103
105
  this.write(str);
104
106
  }
105
107
  }
106
108
 
107
109
  #showAnsweredQuestion(choice) {
108
- const prefix = `${ansi.bold.open}${SYMBOLS.Tick} ${this.message} ${SYMBOLS.Pointer}`;
109
- const formattedChoice = `${ansi.yellow.open}${choice.label ?? choice}${ansi.reset.open}`;
110
+ const prefix = `${SYMBOLS.Tick} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`;
111
+ const formattedChoice = kleur.yellow(choice.label ?? choice);
110
112
 
111
113
  this.write(`${prefix} ${formattedChoice}${EOL}`);
112
114
  }
@@ -175,6 +177,6 @@ export class SelectPrompt extends AbstractPrompt {
175
177
  }
176
178
 
177
179
  #showQuestion() {
178
- this.write(`${ansi.bold.open}${SYMBOLS.QuestionMark} ${this.message}${ansi.bold.close}${EOL}`);
180
+ this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}${EOL}`);
179
181
  }
180
182
  }