@topcli/prompts 1.5.0 → 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.0",
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/",
@@ -30,9 +30,8 @@
30
30
  "esmock": "^2.5.2"
31
31
  },
32
32
  "dependencies": {
33
- "is-unicode-supported": "^1.3.0",
34
- "kleur": "^4.1.5",
35
- "strip-ansi": "^7.1.0"
33
+ "@topcli/wcwidth": "^1.0.1",
34
+ "kleur": "^4.1.5"
36
35
  },
37
36
  "engines": {
38
37
  "node": ">=14"
package/src/constants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // Import Third-party Dependencies
2
2
  import kleur from "kleur";
3
- import isUnicodeSupported from "is-unicode-supported";
3
+
4
+ // Import Internal Dependencies
5
+ import { isUnicodeSupported } from "./utils.js";
4
6
 
5
7
  const kMainSymbols = {
6
8
  tick: "✔",
@@ -20,7 +22,7 @@ const kFallbackSymbols = {
20
22
  active: "(+)",
21
23
  inactive: "(-)"
22
24
  };
23
- const kSymbols = isUnicodeSupported() ? kMainSymbols : kFallbackSymbols;
25
+ const kSymbols = isUnicodeSupported() || process.env.CI ? kMainSymbols : kFallbackSymbols;
24
26
  const kPointer = kleur.gray(kSymbols.pointer);
25
27
 
26
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,10 +3,12 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import wcwidth from "@topcli/wcwidth";
6
7
 
7
8
  // Import Internal Dependencies
8
- import { AbstractPrompt } from "./abstract-prompt.js";
9
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
10
12
 
11
13
  // CONSTANTS
12
14
  const kToggleKeys = new Set([
@@ -65,7 +67,10 @@ export class ConfirmPrompt extends AbstractPrompt {
65
67
  }
66
68
 
67
69
  #onKeypress(resolve, value, key) {
68
- this.stdout.moveCursor(-this.#getQuestionQuery().length, 0);
70
+ this.stdout.moveCursor(
71
+ -this.stdout.columns,
72
+ -Math.floor(wcwidth(stripAnsi(this.#getQuestionQuery())) / this.stdout.columns)
73
+ );
69
74
  this.stdout.clearScreenDown();
70
75
 
71
76
  if (key.name === "return") {
@@ -92,7 +97,11 @@ export class ConfirmPrompt extends AbstractPrompt {
92
97
  }
93
98
 
94
99
  #onQuestionAnswer() {
95
- this.clearLastLine();
100
+ this.stdout.moveCursor(
101
+ -this.stdout.columns,
102
+ -(Math.floor(wcwidth(stripAnsi(this.#getQuestionQuery())) / this.stdout.columns) || 1)
103
+ );
104
+ this.stdout.clearScreenDown();
96
105
  this.write(`${this.selectedValue ? SYMBOLS.Tick : SYMBOLS.Cross} ${kleur.bold(this.message)}${EOL}`);
97
106
  }
98
107
 
@@ -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
+ };
@@ -0,0 +1,333 @@
1
+ // Import Node.js Dependencies
2
+ import { EOL } from "node:os";
3
+
4
+ // Import Third-party Dependencies
5
+ import kleur from "kleur";
6
+ import wcwidth from "@topcli/wcwidth";
7
+
8
+ // Import Internal Dependencies
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"];
15
+
16
+ export class MultiselectPrompt extends AbstractPrompt {
17
+ #boundExitEvent = () => void 0;
18
+ #boundKeyPressEvent = () => void 0;
19
+ #validators;
20
+
21
+ activeIndex = 0;
22
+ selectedIndexes = [];
23
+ questionMessage;
24
+ autocompleteValue = "";
25
+
26
+ get choices() {
27
+ return this.options.choices;
28
+ }
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
+
60
+ constructor(message, options) {
61
+ const {
62
+ stdin = process.stdin,
63
+ stdout = process.stdout,
64
+ choices,
65
+ preSelectedChoices,
66
+ validators = []
67
+ } = options ?? {};
68
+
69
+ super(message, stdin, stdout);
70
+
71
+ if (!options) {
72
+ this.destroy();
73
+ throw new TypeError("Missing required options");
74
+ }
75
+
76
+ this.options = options;
77
+
78
+ if (!choices?.length) {
79
+ this.destroy();
80
+ throw new TypeError("Missing required param: choices");
81
+ }
82
+
83
+ this.#validators = validators;
84
+
85
+ for (const choice of choices) {
86
+ if (typeof choice === "string") {
87
+ continue;
88
+ }
89
+
90
+ for (const prop of kRequiredChoiceProperties) {
91
+ if (!choice[prop]) {
92
+ this.destroy();
93
+ throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ if (!preSelectedChoices) {
99
+ return;
100
+ }
101
+
102
+ for (const choice of preSelectedChoices) {
103
+ const choiceIndex = this.filteredChoices.findIndex((item) => {
104
+ if (typeof item === "string") {
105
+ return item === choice;
106
+ }
107
+
108
+ return item.value === choice;
109
+ });
110
+
111
+ if (choiceIndex === -1) {
112
+ throw new Error(`Invalid pre-selected choice: ${choice.value ?? choice}`);
113
+ }
114
+
115
+ this.selectedIndexes.push(choiceIndex);
116
+ }
117
+ }
118
+
119
+ #getFormattedChoice(choiceIndex) {
120
+ const choice = this.filteredChoices[choiceIndex];
121
+
122
+ if (typeof choice === "string") {
123
+ return { value: choice, label: choice };
124
+ }
125
+
126
+ return choice;
127
+ }
128
+
129
+ #getVisibleChoices() {
130
+ const maxVisible = this.options.maxVisible || 8;
131
+ let startIndex = Math.min(this.filteredChoices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
132
+ if (startIndex < 0) {
133
+ startIndex = 0;
134
+ }
135
+
136
+ const endIndex = Math.min(startIndex + maxVisible, this.filteredChoices.length);
137
+
138
+ return { startIndex, endIndex };
139
+ }
140
+
141
+ #showChoices() {
142
+ const { startIndex, endIndex } = this.#getVisibleChoices();
143
+ this.lastRender = { startIndex, endIndex };
144
+
145
+ if (this.options.autocomplete) {
146
+ this.write(`${SYMBOLS.Pointer} ${this.autocompleteValue}${EOL}`);
147
+ }
148
+ for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
149
+ const choice = this.#getFormattedChoice(choiceIndex);
150
+ const isChoiceActive = choiceIndex === this.activeIndex;
151
+ const isChoiceSelected = this.selectedIndexes.includes(choiceIndex);
152
+ const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
153
+ const showNextChoicesArrow = endIndex < this.filteredChoices.length && choiceIndex === endIndex - 1;
154
+
155
+ let prefixArrow = " ";
156
+ if (showPreviousChoicesArrow) {
157
+ prefixArrow = SYMBOLS.Previous + " ";
158
+ }
159
+ else if (showNextChoicesArrow) {
160
+ prefixArrow = SYMBOLS.Next + " ";
161
+ }
162
+
163
+ const prefix = `${prefixArrow}${isChoiceSelected ? SYMBOLS.Active : SYMBOLS.Inactive}`;
164
+ const formattedLabel = choice.label.padEnd(
165
+ this.longestChoice < 10 ? this.longestChoice : 0
166
+ );
167
+ const formattedDescription = choice.description ? ` - ${choice.description}` : "";
168
+ const color = isChoiceActive ? kleur.white().bold : kleur.gray;
169
+ const str = `${prefix} ${color(`${formattedLabel}${formattedDescription}`)}${EOL}`;
170
+
171
+ this.write(str);
172
+ }
173
+ }
174
+
175
+ #showAnsweredQuestion(choices, isAgentAnswer = false) {
176
+ const prefixSymbol = this.selectedIndexes.length === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick;
177
+ const prefix = `${prefixSymbol} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`;
178
+ const formattedChoice = kleur.yellow(choices);
179
+
180
+ this.write(`${prefix}${choices ? ` ${formattedChoice}` : ""}${EOL}`);
181
+ }
182
+
183
+ #onProcessExit() {
184
+ this.stdin.off("keypress", this.#boundKeyPressEvent);
185
+ this.stdout.moveCursor(-this.stdout.columns, 0);
186
+ this.stdout.clearScreenDown();
187
+ this.write(SYMBOLS.ShowCursor);
188
+ }
189
+
190
+ #onKeypress(...args) {
191
+ const [resolve, render, _, key] = args;
192
+ if (key.name === "up") {
193
+ this.activeIndex = this.activeIndex === 0 ? this.filteredChoices.length - 1 : this.activeIndex - 1;
194
+ render();
195
+ }
196
+ else if (key.name === "down") {
197
+ this.activeIndex = this.activeIndex === this.filteredChoices.length - 1 ? 0 : this.activeIndex + 1;
198
+ render();
199
+ }
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);
203
+ render();
204
+ }
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);
211
+ render();
212
+ }
213
+ else if (key.name === "return") {
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]);
216
+
217
+ for (const validator of this.#validators) {
218
+ if (!validator.validate(values)) {
219
+ const error = validator.error(values);
220
+ render({ error });
221
+
222
+ return;
223
+ }
224
+ }
225
+
226
+ render({ clearRender: true });
227
+
228
+ this.#showAnsweredQuestion(labels.join(", "));
229
+
230
+ this.write(SYMBOLS.ShowCursor);
231
+ this.destroy();
232
+
233
+ this.#onProcessExit();
234
+ process.off("exit", this.#boundExitEvent);
235
+
236
+ resolve(values);
237
+ }
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
+ }
250
+ render();
251
+ }
252
+ }
253
+
254
+ async multiselect() {
255
+ if (this.agent.nextAnswers.length > 0) {
256
+ const answer = this.agent.nextAnswers.shift();
257
+ this.#showAnsweredQuestion(answer, true);
258
+ this.destroy();
259
+
260
+ return answer;
261
+ }
262
+
263
+ this.write(SYMBOLS.HideCursor);
264
+ this.#showQuestion();
265
+
266
+ const render = (options = {}) => {
267
+ const {
268
+ initialRender = false,
269
+ clearRender = false,
270
+ error = null
271
+ } = options;
272
+
273
+ if (!initialRender) {
274
+ let linesToClear = this.lastRender.endIndex - this.lastRender.startIndex;
275
+ while (linesToClear > 0) {
276
+ this.clearLastLine();
277
+ linesToClear--;
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
+ }
288
+ }
289
+
290
+ if (clearRender) {
291
+ const questionLineCount = Math.ceil(
292
+ wcwidth(stripAnsi(this.questionMessage)) / this.stdout.columns
293
+ );
294
+ this.stdout.moveCursor(-this.stdout.columns, -(1 + questionLineCount));
295
+ this.stdout.clearScreenDown();
296
+
297
+ return;
298
+ }
299
+
300
+ if (error) {
301
+ const linesToClear = Math.ceil(wcwidth(this.questionMessage) / this.stdout.columns) + 1;
302
+ this.stdout.moveCursor(0, -linesToClear);
303
+ this.stdout.clearScreenDown();
304
+ this.#showQuestion(error);
305
+ }
306
+
307
+ this.#showChoices();
308
+ };
309
+
310
+ render({ initialRender: true });
311
+
312
+ return new Promise((resolve) => {
313
+ this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
314
+ this.stdin.on("keypress", this.#boundKeyPressEvent);
315
+
316
+ this.#boundExitEvent = this.#onProcessExit.bind(this);
317
+ process.once("exit", this.#boundExitEvent);
318
+ });
319
+ }
320
+
321
+ #showQuestion(error = null) {
322
+ let hint = kleur.gray(
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)`
325
+ );
326
+ if (error) {
327
+ hint += ` ${kleur.red().bold(`[${error}]`)}`;
328
+ }
329
+ this.questionMessage = `${SYMBOLS.QuestionMark} ${kleur.bold(this.message)} ${hint}`;
330
+
331
+ this.write(`${this.questionMessage}${EOL}`);
332
+ }
333
+ }
@@ -3,10 +3,12 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import wcwidth from "@topcli/wcwidth";
6
7
 
7
8
  // Import Internal Dependencies
8
- import { AbstractPrompt } from "./abstract-prompt.js";
9
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
10
12
 
11
13
  export class QuestionPrompt extends AbstractPrompt {
12
14
  #validators;
@@ -64,7 +66,12 @@ export class QuestionPrompt extends AbstractPrompt {
64
66
  }
65
67
 
66
68
  #onQuestionAnswer() {
67
- this.clearLastLine();
69
+ const questionLineCount = Math.ceil(
70
+ wcwidth(stripAnsi(this.#getQuestionQuery() + this.answer)) / this.stdout.columns
71
+ );
72
+
73
+ this.stdout.moveCursor(-this.stdout.columns, -questionLineCount);
74
+ this.stdout.clearScreenDown();
68
75
 
69
76
  for (const validator of this.#validators) {
70
77
  if (!validator.validate(this.answer)) {
@@ -3,12 +3,16 @@ import { EOL } from "node:os";
3
3
 
4
4
  // Import Third-party Dependencies
5
5
  import kleur from "kleur";
6
+ import wcwidth from "@topcli/wcwidth";
6
7
 
7
8
  // Import Internal Dependencies
8
- import { AbstractPrompt } from "./abstract-prompt.js";
9
- import { SYMBOLS } from "./constants.js";
9
+ import { AbstractPrompt } from "./abstract.js";
10
+ import { stripAnsi } from "../utils.js";
11
+ import { SYMBOLS } from "../constants.js";
10
12
 
11
13
  export class SelectPrompt extends AbstractPrompt {
14
+ #boundExitEvent = () => void 0;
15
+ #boundKeyPressEvent = () => void 0;
12
16
  activeIndex = 0;
13
17
 
14
18
  get choices() {
@@ -113,6 +117,46 @@ export class SelectPrompt extends AbstractPrompt {
113
117
  this.write(`${prefix} ${formattedChoice}${EOL}`);
114
118
  }
115
119
 
120
+ #onProcessExit() {
121
+ this.stdin.off("keypress", this.#boundKeyPressEvent);
122
+ this.stdout.moveCursor(-this.stdout.columns, 0);
123
+ this.stdout.clearScreenDown();
124
+ this.write(SYMBOLS.ShowCursor);
125
+ }
126
+
127
+ #onKeypress(...args) {
128
+ const [resolve, render, _, key] = args;
129
+
130
+ if (key.name === "up") {
131
+ this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
132
+ render();
133
+ }
134
+ else if (key.name === "down") {
135
+ this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
136
+ render();
137
+ }
138
+ else if (key.name === "return") {
139
+ render({ clearRender: true });
140
+
141
+ const currentChoice = this.choices[this.activeIndex];
142
+ const value = currentChoice.value ?? currentChoice;
143
+
144
+ if (!this.options.ignoreValues?.includes(value)) {
145
+ this.#showAnsweredQuestion(currentChoice);
146
+ }
147
+
148
+ this.write(SYMBOLS.ShowCursor);
149
+ this.destroy();
150
+
151
+ this.#onProcessExit();
152
+ process.off("exit", this.#boundExitEvent);
153
+ resolve(value);
154
+ }
155
+ else {
156
+ render();
157
+ }
158
+ }
159
+
116
160
  async select() {
117
161
  if (this.agent.nextAnswers.length > 0) {
118
162
  const answer = this.agent.nextAnswers.shift();
@@ -140,7 +184,11 @@ export class SelectPrompt extends AbstractPrompt {
140
184
  }
141
185
 
142
186
  if (clearRender) {
143
- this.stdout.moveCursor(0, -2);
187
+ const questionLineCount = Math.ceil(
188
+ wcwidth(stripAnsi(this.questionMessage)) / this.stdout.columns
189
+ );
190
+
191
+ this.stdout.moveCursor(-this.stdout.columns, -(1 + questionLineCount));
144
192
  this.stdout.clearScreenDown();
145
193
 
146
194
  return;
@@ -152,39 +200,16 @@ export class SelectPrompt extends AbstractPrompt {
152
200
  render({ initialRender: true });
153
201
 
154
202
  return new Promise((resolve) => {
155
- const onKeypress = (value, key) => {
156
- if (key.name === "up") {
157
- this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
158
- render();
159
- }
160
- else if (key.name === "down") {
161
- this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
162
- render();
163
- }
164
- else if (key.name === "return") {
165
- this.stdin.off("keypress", onKeypress);
166
-
167
- render({ clearRender: true });
168
-
169
- const currentChoice = this.choices[this.activeIndex];
170
- const value = currentChoice.value ?? currentChoice;
171
-
172
- if (!this.options.ignoreValues?.includes(value)) {
173
- this.#showAnsweredQuestion(currentChoice);
174
- }
175
-
176
- this.write(SYMBOLS.ShowCursor);
177
- this.destroy();
178
-
179
- resolve(value);
180
- }
181
- };
203
+ this.#boundKeyPressEvent = this.#onKeypress.bind(this, resolve, render);
204
+ this.stdin.on("keypress", this.#boundKeyPressEvent);
182
205
 
183
- this.stdin.on("keypress", onKeypress);
206
+ this.#boundExitEvent = this.#onProcessExit.bind(this);
207
+ process.once("exit", this.#boundExitEvent);
184
208
  });
185
209
  }
186
210
 
187
211
  #showQuestion() {
188
- this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}${EOL}`);
212
+ this.questionMessage = `${SYMBOLS.QuestionMark} ${kleur.bold(this.message)}`;
213
+ this.write(`${this.questionMessage}${EOL}`);
189
214
  }
190
215
  }
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
+ }
@@ -1,257 +0,0 @@
1
- // Import Node.js Dependencies
2
- import { EOL } from "node:os";
3
-
4
- // Import Third-party Dependencies
5
- import kleur from "kleur";
6
-
7
- // Import Internal Dependencies
8
- import { AbstractPrompt } from "./abstract-prompt.js";
9
- import { SYMBOLS } from "./constants.js";
10
-
11
- export class MultiselectPrompt extends AbstractPrompt {
12
- #validators;
13
-
14
- activeIndex = 0;
15
- selectedIndexes = [];
16
-
17
- get choices() {
18
- return this.options.choices;
19
- }
20
-
21
- constructor(message, options) {
22
- const {
23
- stdin = process.stdin,
24
- stdout = process.stdout,
25
- choices,
26
- preSelectedChoices,
27
- validators = []
28
- } = options ?? {};
29
-
30
- super(message, stdin, stdout);
31
-
32
- if (!options) {
33
- this.destroy();
34
- throw new TypeError("Missing required options");
35
- }
36
-
37
- this.options = options;
38
-
39
- if (!choices?.length) {
40
- this.destroy();
41
- throw new TypeError("Missing required param: choices");
42
- }
43
-
44
- this.longestChoice = Math.max(...choices.map((choice) => {
45
- if (typeof choice === "string") {
46
- return choice.length;
47
- }
48
-
49
- const kRequiredChoiceProperties = ["label", "value"];
50
-
51
- for (const prop of kRequiredChoiceProperties) {
52
- if (!choice[prop]) {
53
- this.destroy();
54
- throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`);
55
- }
56
- }
57
-
58
- return choice.label.length;
59
- }));
60
-
61
- this.#validators = validators;
62
-
63
- if (!preSelectedChoices) {
64
- return;
65
- }
66
-
67
- for (const choice of preSelectedChoices) {
68
- const choiceIndex = this.choices.findIndex((item) => {
69
- if (typeof item === "string") {
70
- return item === choice;
71
- }
72
-
73
- return item.value === choice;
74
- });
75
-
76
- if (choiceIndex === -1) {
77
- throw new Error(`Invalid pre-selected choice: ${choice.value ?? choice}`);
78
- }
79
-
80
- this.selectedIndexes.push(choiceIndex);
81
- }
82
- }
83
-
84
- #getFormattedChoice(choiceIndex) {
85
- const choice = this.choices[choiceIndex];
86
-
87
- if (typeof choice === "string") {
88
- return { value: choice, label: choice };
89
- }
90
-
91
- return choice;
92
- }
93
-
94
- #getVisibleChoices() {
95
- const maxVisible = this.options.maxVisible || 8;
96
- let startIndex = Math.min(this.choices.length - maxVisible, this.activeIndex - Math.floor(maxVisible / 2));
97
- if (startIndex < 0) {
98
- startIndex = 0;
99
- }
100
-
101
- const endIndex = Math.min(startIndex + maxVisible, this.choices.length);
102
-
103
- return { startIndex, endIndex };
104
- }
105
-
106
- #showChoices() {
107
- const { startIndex, endIndex } = this.#getVisibleChoices();
108
- this.lastRender = { startIndex, endIndex };
109
-
110
- for (let choiceIndex = startIndex; choiceIndex < endIndex; choiceIndex++) {
111
- const choice = this.#getFormattedChoice(choiceIndex);
112
- const isChoiceActive = choiceIndex === this.activeIndex;
113
- const isChoiceSelected = this.selectedIndexes.includes(choiceIndex);
114
- const showPreviousChoicesArrow = startIndex > 0 && choiceIndex === startIndex;
115
- const showNextChoicesArrow = endIndex < this.choices.length && choiceIndex === endIndex - 1;
116
-
117
- let prefixArrow = " ";
118
- if (showPreviousChoicesArrow) {
119
- prefixArrow = SYMBOLS.Previous + " ";
120
- }
121
- else if (showNextChoicesArrow) {
122
- prefixArrow = SYMBOLS.Next + " ";
123
- }
124
-
125
- const prefix = `${prefixArrow}${isChoiceSelected ? SYMBOLS.Active : SYMBOLS.Inactive}`;
126
- const formattedLabel = choice.label.padEnd(
127
- this.longestChoice < 10 ? this.longestChoice : 0
128
- );
129
- const formattedDescription = choice.description ? ` - ${choice.description}` : "";
130
- const color = isChoiceActive ? kleur.white().bold : kleur.gray;
131
- const str = `${prefix} ${color(`${formattedLabel}${formattedDescription}`)}${EOL}`;
132
-
133
- this.write(str);
134
- }
135
- }
136
-
137
- #showAnsweredQuestion(choices, isAgentAnswer = false) {
138
- const prefixSymbol = this.selectedIndexes.length === 0 && !isAgentAnswer ? SYMBOLS.Cross : SYMBOLS.Tick;
139
- const prefix = `${prefixSymbol} ${kleur.bold(this.message)} ${SYMBOLS.Pointer}`;
140
- const formattedChoice = kleur.yellow(choices);
141
-
142
- this.write(`${prefix}${choices ? ` ${formattedChoice}` : ""}${EOL}`);
143
- }
144
-
145
- async multiselect() {
146
- if (this.agent.nextAnswers.length > 0) {
147
- const answer = this.agent.nextAnswers.shift();
148
- this.#showAnsweredQuestion(answer, true);
149
- this.destroy();
150
-
151
- return answer;
152
- }
153
-
154
- this.write(SYMBOLS.HideCursor);
155
- this.#showQuestion();
156
-
157
- const render = (options = {}) => {
158
- const {
159
- initialRender = false,
160
- clearRender = false,
161
- error = null
162
- } = options;
163
-
164
- if (!initialRender) {
165
- let linesToClear = this.lastRender.endIndex - this.lastRender.startIndex;
166
- while (linesToClear > 0) {
167
- this.clearLastLine();
168
- linesToClear--;
169
- }
170
- }
171
-
172
- if (clearRender) {
173
- this.stdout.moveCursor(0, -2);
174
- this.stdout.clearScreenDown();
175
-
176
- return;
177
- }
178
-
179
- if (error) {
180
- this.stdout.moveCursor(0, -2);
181
- this.stdout.clearScreenDown();
182
- this.#showQuestion(error);
183
- }
184
-
185
- this.#showChoices();
186
- };
187
-
188
- render({ initialRender: true });
189
-
190
- return new Promise((resolve) => {
191
- const onKeypress = (value, key) => {
192
- if (key.name === "up") {
193
- this.activeIndex = this.activeIndex === 0 ? this.choices.length - 1 : this.activeIndex - 1;
194
- render();
195
- }
196
- else if (key.name === "down") {
197
- this.activeIndex = this.activeIndex === this.choices.length - 1 ? 0 : this.activeIndex + 1;
198
- render();
199
- }
200
- else if (key.name === "a") {
201
- this.selectedIndexes = this.selectedIndexes.length === this.choices.length ? [] : this.choices.map((_, index) => index);
202
- render();
203
- }
204
- else if (key.name === "space") {
205
- const isChoiceSelected = this.selectedIndexes.includes(this.activeIndex);
206
-
207
- if (isChoiceSelected) {
208
- this.selectedIndexes = this.selectedIndexes.filter((index) => index !== this.activeIndex);
209
- }
210
- else {
211
- this.selectedIndexes.push(this.activeIndex);
212
- }
213
-
214
- render();
215
- }
216
- else if (key.name === "return") {
217
- const labels = this.selectedIndexes.map((index) => this.choices[index].label ?? this.choices[index]);
218
- const values = this.selectedIndexes.map((index) => this.choices[index].value ?? this.choices[index]);
219
-
220
- for (const validator of this.#validators) {
221
- if (!validator.validate(values)) {
222
- const error = validator.error(values);
223
- render({ error });
224
-
225
- return;
226
- }
227
- }
228
-
229
- this.stdin.off("keypress", onKeypress);
230
-
231
- render({ clearRender: true });
232
-
233
-
234
- this.#showAnsweredQuestion(labels.join(", "));
235
-
236
- this.write(SYMBOLS.ShowCursor);
237
- this.destroy();
238
-
239
- resolve(values);
240
- }
241
- };
242
-
243
- this.stdin.on("keypress", onKeypress);
244
- });
245
- }
246
-
247
- #showQuestion(error = null) {
248
- let hint = kleur.gray(
249
- `(Press ${kleur.bold("<a>")} to toggle all, ${kleur.bold("<space>")} to select, ${kleur.bold("<return>")} to submit)`
250
- );
251
- if (error) {
252
- hint += ` ${kleur.red().bold(`[${error}]`)}`;
253
- }
254
-
255
- this.write(`${SYMBOLS.QuestionMark} ${kleur.bold(this.message)} ${hint}${EOL}`);
256
- }
257
- }