@topcli/prompts 1.5.1 → 1.6.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
@@ -108,6 +108,8 @@ const os = await multiselect('Choose OS', {
108
108
  });
109
109
  ```
110
110
 
111
+ Use `autocomplete` to allow filtered choices. This can be usefull for a large list of choices.
112
+
111
113
  ### `confirm()`
112
114
 
113
115
  ```ts
@@ -177,6 +179,7 @@ export interface MultiselectOptions extends SharedOptions {
177
179
  maxVisible?: number;
178
180
  preSelectedChoices?: (Choice | string)[];
179
181
  validators?: Validator[];
182
+ autocomplete?: boolean;
180
183
  }
181
184
 
182
185
  export interface ConfirmOptions extends SharedOptions {
package/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export interface MultiselectOptions extends SharedOptions {
35
35
  maxVisible?: number;
36
36
  preSelectedChoices?: (Choice | string)[];
37
37
  validators?: Validator[];
38
+ autocomplete?: boolean;
38
39
  }
39
40
 
40
41
  export interface ConfirmOptions extends SharedOptions {
package/index.js CHANGED
@@ -1,31 +1,28 @@
1
1
  // Import Internal Dependencies
2
- import { ConfirmPrompt } from "./src/confirm-prompt.js";
3
- import { SelectPrompt } from "./src/select-prompt.js";
4
- import { QuestionPrompt } from "./src/question-prompt.js";
2
+ import * as prompts from "./src/prompts/index.js";
5
3
  import { required } from "./src/validators.js";
6
4
  import { PromptAgent } from "./src/prompt-agent.js";
7
- import { MultiselectPrompt } from "./src/multiselect-prompt.js";
8
5
 
9
6
  export async function question(message, options = {}) {
10
- const questionPrompt = new QuestionPrompt(message, options);
7
+ const questionPrompt = new prompts.QuestionPrompt(message, options);
11
8
 
12
9
  return questionPrompt.question();
13
10
  }
14
11
 
15
12
  export async function select(message, options) {
16
- const selectPrompt = new SelectPrompt(message, options);
13
+ const selectPrompt = new prompts.SelectPrompt(message, options);
17
14
 
18
15
  return selectPrompt.select();
19
16
  }
20
17
 
21
18
  export async function confirm(message, options) {
22
- const confirmPrompt = new ConfirmPrompt(message, options);
19
+ const confirmPrompt = new prompts.ConfirmPrompt(message, options);
23
20
 
24
21
  return confirmPrompt.confirm();
25
22
  }
26
23
 
27
24
  export async function multiselect(message, options) {
28
- const multiselectPrompt = new MultiselectPrompt(message, options);
25
+ const multiselectPrompt = new prompts.MultiselectPrompt(message, options);
29
26
 
30
27
  return multiselectPrompt.multiselect();
31
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topcli/prompts",
3
- "version": "1.5.1",
3
+ "version": "1.6.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/",
@@ -31,10 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@topcli/wcwidth": "^1.0.1",
34
- "is-ci": "^3.0.1",
35
- "is-unicode-supported": "^2.0.0",
36
- "kleur": "^4.1.5",
37
- "strip-ansi": "^7.1.0"
34
+ "kleur": "^4.1.5"
38
35
  },
39
36
  "engines": {
40
37
  "node": ">=14"
package/src/constants.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Import Third-party Dependencies
2
2
  import kleur from "kleur";
3
- import isUnicodeSupported from "is-unicode-supported";
4
- import isCI from "is-ci";
3
+
4
+ // Import Internal Dependencies
5
+ import { isUnicodeSupported } from "./utils.js";
5
6
 
6
7
  const kMainSymbols = {
7
8
  tick: "✔",
@@ -21,7 +22,7 @@ const kFallbackSymbols = {
21
22
  active: "(+)",
22
23
  inactive: "(-)"
23
24
  };
24
- const kSymbols = isUnicodeSupported() || isCI ? kMainSymbols : kFallbackSymbols;
25
+ const kSymbols = isUnicodeSupported() || process.env.CI ? kMainSymbols : kFallbackSymbols;
25
26
  const kPointer = kleur.gray(kSymbols.pointer);
26
27
 
27
28
  export const SYMBOLS = {
@@ -3,11 +3,9 @@ import { EOL } from "node:os";
3
3
  import { createInterface } from "node:readline";
4
4
  import { Writable } from "node:stream";
5
5
 
6
- // Import Third-party Dependencies
7
- import stripAnsi from "strip-ansi";
8
-
9
6
  // Import Internal Dependencies
10
- import { PromptAgent } from "./prompt-agent.js";
7
+ import { stripAnsi } from "../utils.js";
8
+ import { PromptAgent } from "../prompt-agent.js";
11
9
 
12
10
  export class AbstractPrompt {
13
11
  constructor(message, input = process.stdin, output = process.stdout) {
@@ -3,12 +3,12 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
- import stripAnsi from "strip-ansi";
7
6
  import wcwidth from "@topcli/wcwidth";
8
7
 
9
8
  // Import Internal Dependencies
10
- import { AbstractPrompt } from "./abstract-prompt.js";
11
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
12
12
 
13
13
  // CONSTANTS
14
14
  const kToggleKeys = new Set([
@@ -0,0 +1,12 @@
1
+ // Import Internal Dependencies
2
+ import { QuestionPrompt } from "./question.js";
3
+ import { ConfirmPrompt } from "./confirm.js";
4
+ import { SelectPrompt } from "./select.js";
5
+ import { MultiselectPrompt } from "./multiselect.js";
6
+
7
+ export {
8
+ QuestionPrompt,
9
+ ConfirmPrompt,
10
+ SelectPrompt,
11
+ MultiselectPrompt
12
+ };
@@ -3,12 +3,15 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
- import stripAnsi from "strip-ansi";
7
6
  import wcwidth from "@topcli/wcwidth";
8
7
 
9
8
  // Import Internal Dependencies
10
- import { AbstractPrompt } from "./abstract-prompt.js";
11
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
12
+
13
+ // CONSTANTS
14
+ const kRequiredChoiceProperties = ["label", "value"];
12
15
 
13
16
  export class MultiselectPrompt extends AbstractPrompt {
14
17
  #boundExitEvent = () => void 0;
@@ -18,11 +21,42 @@ export class MultiselectPrompt extends AbstractPrompt {
18
21
  activeIndex = 0;
19
22
  selectedIndexes = [];
20
23
  questionMessage;
24
+ autocompleteValue = "";
21
25
 
22
26
  get choices() {
23
27
  return this.options.choices;
24
28
  }
25
29
 
30
+ get filteredChoices() {
31
+ return this.options.autocomplete && this.autocompleteValue.length > 0 ? this.choices.filter((choice) => {
32
+ if (typeof choice === "string") {
33
+ if (this.autocompleteValue.includes(" ")) {
34
+ return this.autocompleteValue.split(" ").every((word) => choice.includes(word)) ||
35
+ choice.includes(this.autocompleteValue);
36
+ }
37
+
38
+ return choice.includes(this.autocompleteValue);
39
+ }
40
+
41
+ if (this.autocompleteValue.includes(" ")) {
42
+ return this.autocompleteValue.split(" ").every((word) => choice.label.includes(word)) ||
43
+ choice.label.includes(this.autocompleteValue);
44
+ }
45
+
46
+ return choice.label.includes(this.autocompleteValue);
47
+ }) : this.choices;
48
+ }
49
+
50
+ get longestChoice() {
51
+ return Math.max(...this.filteredChoices.map((choice) => {
52
+ if (typeof choice === "string") {
53
+ return choice.length;
54
+ }
55
+
56
+ return choice.label.length;
57
+ }));
58
+ }
59
+
26
60
  constructor(message, options) {
27
61
  const {
28
62
  stdin = process.stdin,
@@ -46,31 +80,27 @@ export class MultiselectPrompt extends AbstractPrompt {
46
80
  throw new TypeError("Missing required param: choices");
47
81
  }
48
82
 
49
- this.longestChoice = Math.max(...choices.map((choice) => {
83
+ this.#validators = validators;
84
+
85
+ for (const choice of choices) {
50
86
  if (typeof choice === "string") {
51
- return choice.length;
87
+ continue;
52
88
  }
53
89
 
54
- const kRequiredChoiceProperties = ["label", "value"];
55
-
56
90
  for (const prop of kRequiredChoiceProperties) {
57
91
  if (!choice[prop]) {
58
92
  this.destroy();
59
93
  throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
60
94
  }
61
95
  }
62
-
63
- return choice.label.length;
64
- }));
65
-
66
- this.#validators = validators;
96
+ }
67
97
 
68
98
  if (!preSelectedChoices) {
69
99
  return;
70
100
  }
71
101
 
72
102
  for (const choice of preSelectedChoices) {
73
- const choiceIndex = this.choices.findIndex((item) => {
103
+ const choiceIndex = this.filteredChoices.findIndex((item) => {
74
104
  if (typeof item === "string") {
75
105
  return item === choice;
76
106
  }
@@ -87,7 +117,7 @@ export class MultiselectPrompt extends AbstractPrompt {
87
117
  }
88
118
 
89
119
  #getFormattedChoice(choiceIndex) {
90
- const choice = this.choices[choiceIndex];
120
+ const choice = this.filteredChoices[choiceIndex];
91
121
 
92
122
  if (typeof choice === "string") {
93
123
  return { value: choice, label: choice };
@@ -98,12 +128,12 @@ export class MultiselectPrompt extends AbstractPrompt {
98
128
 
99
129
  #getVisibleChoices() {
100
130
  const maxVisible = this.options.maxVisible || 8;
101
- let startIndex = Math.min(this.choices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
131
+ let startIndex = Math.min(this.filteredChoices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
102
132
  if (startIndex < 0) {
103
133
  startIndex = 0;
104
134
  }
105
135
 
106
- const endIndex = Math.min(startIndex + maxVisible, this.choices.length);
136
+ const endIndex = Math.min(startIndex + maxVisible, this.filteredChoices.length);
107
137
 
108
138
  return { startIndex, endIndex };
109
139
  }
@@ -112,12 +142,15 @@ export class MultiselectPrompt extends AbstractPrompt {
112
142
  const { startIndex, endIndex } = this.#getVisibleChoices();
113
143
  this.lastRender = { startIndex, endIndex };
114
144
 
145
+ if (this.options.autocomplete) {
146
+ this.write(`${SYMBOLS.Pointer} ${this.autocompleteValue}${EOL}`);
147
+ }
115
148
  for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
116
149
  const choice = this.#getFormattedChoice(choiceIndex);
117
150
  const isChoiceActive = choiceIndex === this.activeIndex;
118
151
  const isChoiceSelected = this.selectedIndexes.includes(choiceIndex);
119
152
  const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
120
- const showNextChoicesArrow = endIndex < this.choices.length && choiceIndex === endIndex - 1;
153
+ const showNextChoicesArrow = endIndex < this.filteredChoices.length && choiceIndex === endIndex - 1;
121
154
 
122
155
  let prefixArrow = " ";
123
156
  if (showPreviousChoicesArrow) {
@@ -156,34 +189,30 @@ export class MultiselectPrompt extends AbstractPrompt {
156
189
 
157
190
  #onKeypress(...args) {
158
191
  const [resolve, render, _, key] = args;
159
-
160
192
  if (key.name === "up") {
161
- this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
193
+ this.activeIndex = this.activeIndex === 0 ? this.filteredChoices.length - 1 : this.activeIndex - 1;
162
194
  render();
163
195
  }
164
196
  else if (key.name === "down") {
165
- this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
197
+ this.activeIndex = this.activeIndex === this.filteredChoices.length - 1 ? 0 : this.activeIndex + 1;
166
198
  render();
167
199
  }
168
- else if (key.name === "a") {
169
- this.selectedIndexes = this.selectedIndexes.length === this.choices.length ? [] : this.choices.map((_, index) => index);
200
+ else if (key.ctrl && key.name === "a") {
201
+ // eslint-disable-next-line max-len
202
+ this.selectedIndexes = this.selectedIndexes.length === this.filteredChoices.length ? [] : this.filteredChoices.map((_, index) => index);
170
203
  render();
171
204
  }
172
- else if (key.name === "space") {
173
- const isChoiceSelected = this.selectedIndexes.includes(this.activeIndex);
174
-
175
- if (isChoiceSelected) {
176
- this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
177
- }
178
- else {
179
- this.selectedIndexes.push(this.activeIndex);
180
- }
181
-
205
+ else if (key.name === "right") {
206
+ this.selectedIndexes.push(this.activeIndex);
207
+ render();
208
+ }
209
+ else if (key.name === "left") {
210
+ this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
182
211
  render();
183
212
  }
184
213
  else if (key.name === "return") {
185
- const labels = this.selectedIndexes.map((index) => this.choices[index].label ?? this.choices[index]);
186
- const values = this.selectedIndexes.map((index) => this.choices[index].value ?? this.choices[index]);
214
+ const labels = this.selectedIndexes.map((index) => this.filteredChoices[index].label ?? this.filteredChoices[index]);
215
+ const values = this.selectedIndexes.map((index) => this.filteredChoices[index].value ?? this.filteredChoices[index]);
187
216
 
188
217
  for (const validator of this.#validators) {
189
218
  if (!validator.validate(values)) {
@@ -207,6 +236,17 @@ export class MultiselectPrompt extends AbstractPrompt {
207
236
  resolve(values);
208
237
  }
209
238
  else {
239
+ if (!key.ctrl && this.options.autocomplete) {
240
+ // reset selected choices when user type
241
+ this.selectedIndexes = [];
242
+ this.activeIndex = 0;
243
+ if (key.name === "backspace" && this.autocompleteValue.length > 0) {
244
+ this.autocompleteValue = this.autocompleteValue.slice(0, -1);
245
+ }
246
+ else if (key.name !== "backspace") {
247
+ this.autocompleteValue += key.sequence;
248
+ }
249
+ }
210
250
  render();
211
251
  }
212
252
  }
@@ -236,6 +276,15 @@ export class MultiselectPrompt extends AbstractPrompt {
236
276
  this.clearLastLine();
237
277
  linesToClear--;
238
278
  }
279
+ if (this.options.autocomplete) {
280
+ let linesToClear = Math.ceil(
281
+ wcwidth(`${SYMBOLS.Pointer} ${this.autocompleteValue}`) / this.stdout.columns
282
+ );
283
+ while (linesToClear > 0) {
284
+ this.clearLastLine();
285
+ linesToClear--;
286
+ }
287
+ }
239
288
  }
240
289
 
241
290
  if (clearRender) {
@@ -249,7 +298,8 @@ export class MultiselectPrompt extends AbstractPrompt {
249
298
  }
250
299
 
251
300
  if (error) {
252
- this.stdout.moveCursor(0, -2);
301
+ const linesToClear = Math.ceil(wcwidth(this.questionMessage) / this.stdout.columns) + 1;
302
+ this.stdout.moveCursor(0, -linesToClear);
253
303
  this.stdout.clearScreenDown();
254
304
  this.#showQuestion(error);
255
305
  }
@@ -270,7 +320,8 @@ export class MultiselectPrompt extends AbstractPrompt {
270
320
 
271
321
  #showQuestion(error = null) {
272
322
  let hint = kleur.gray(
273
- `(Press ${kleur.bold("<a>")} to toggle all, ${kleur.bold("<space>")} to select, ${kleur.bold("<return>")} to submit)`
323
+ // eslint-disable-next-line max-len
324
+ `(Press ${kleur.bold("<Ctrl+A>")} to toggle all, ${kleur.bold("<Ctrl+Space>")} to select, ${kleur.bold("<Left/Right>")} to toggle, ${kleur.bold("<Return>")} to submit)`
274
325
  );
275
326
  if (error) {
276
327
  hint += ` ${kleur.red().bold(`[${error}]`)}`;
@@ -3,12 +3,12 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
- import stripAnsi from "strip-ansi";
7
6
  import wcwidth from "@topcli/wcwidth";
8
7
 
9
8
  // Import Internal Dependencies
10
- import { AbstractPrompt } from "./abstract-prompt.js";
11
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
12
12
 
13
13
  export class QuestionPrompt extends AbstractPrompt {
14
14
  #validators;
@@ -3,12 +3,12 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
- import stripAnsi from "strip-ansi";
7
6
  import wcwidth from "@topcli/wcwidth";
8
7
 
9
8
  // Import Internal Dependencies
10
- import { AbstractPrompt } from "./abstract-prompt.js";
11
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
12
12
 
13
13
  export class SelectPrompt extends AbstractPrompt {
14
14
  #boundExitEvent = () => void 0;
package/src/utils.js ADDED
@@ -0,0 +1,43 @@
1
+ import process from "node:process";
2
+
3
+ // CONSTANTS
4
+ const kAnsiRegex = ansiRegex();
5
+
6
+ /**
7
+ * @see https://github.com/chalk/ansi-regex
8
+ */
9
+ export function ansiRegex({onlyFirst = false} = {}) {
10
+ const pattern = [
11
+ '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
12
+ '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
13
+ ].join('|');
14
+
15
+ return new RegExp(pattern, onlyFirst ? undefined : 'g');
16
+ }
17
+
18
+ /**
19
+ * @param {!string} string
20
+ * @returns {string}
21
+ */
22
+ export function stripAnsi(string) {
23
+ return string.replace(kAnsiRegex, "");
24
+ }
25
+
26
+ /**
27
+ * @see https://github.com/sindresorhus/is-unicode-supported
28
+ * @returns {boolean}
29
+ */
30
+ export function isUnicodeSupported() {
31
+ if (process.platform !== 'win32') {
32
+ return process.env.TERM !== 'linux'; // Linux console (kernel)
33
+ }
34
+
35
+ return Boolean(process.env.WT_SESSION) // Windows Terminal
36
+ || Boolean(process.env.TERMINUS_SUBLIME) // Terminus (<0.2.27)
37
+ || process.env.ConEmuTask === '{cmd::Cmder}' // ConEmu and cmder
38
+ || process.env.TERM_PROGRAM === 'Terminus-Sublime'
39
+ || process.env.TERM_PROGRAM === 'vscode'
40
+ || process.env.TERM === 'xterm-256color'
41
+ || process.env.TERM === 'alacritty'
42
+ || process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
43
+ }