Skip to content

Commit

Permalink
Merge pull request #18 from LexBorisoff/dev
Browse files Browse the repository at this point in the history
Merge dev into main
LexBorisoff authored Jan 23, 2025
2 parents 628e8a9 + a5112d0 commit b5a1fc9
Showing 23 changed files with 269 additions and 99 deletions.
222 changes: 200 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,11 +3,22 @@
![Build](https://img.shields.io/github/actions/workflow/status/LexBorisoff/prompts/release.yml)
![NPM Version](https://img.shields.io/npm/v/@lexjs/prompts)

A wrapper around the [prompts](https://www.npmjs.com/package/prompts) package with abstracted methods.
Interactive prompts

- [Installation](#installation)
- [Usage](#usage)
- [Methods](#methods)
- [`autocomplete`](#autocomplete)
- [`autocompleteMultiselect`](#autocompletemultiselect)
- [`confirm`](#confirm)
- [`date`](#date)
- [`invisible`](#invisible)
- [`list`](#list)
- [`multiselect`](#multiselect)
- [`number`](#number)
- [`password`](#password)
- [`select`](#select)
- [`text`](#text)
- [`toggle`](#toggle)

## Installation

@@ -23,30 +34,197 @@ pnpm add @lexjs/prompts
yarn add @lexjs/prompts
```

## Usage
## Methods

### `autocomplete`

```typescript
import $_ from '@lexjs/prompts';

async main() {
const { answer } = await $_.text({
name: 'answer',
message: 'Type something',
});
}
const result = await $_.autocomplete({
message: 'Choose a city',
name: 'city',
choices: [
{ title: 'New York City' },
{ title: 'Toronto' },
{ title: 'London' },
{ title: 'Paris' },
],
});
```

## Methods
<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/autocomplete.gif?raw=true" alt="autocomplete prompt" width="500" />

### `autocompleteMultiselect`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.autocompleteMultiselect({
message: 'Pick colors',
name: 'colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#008000' },
{ title: 'Blue', value: '#0000ff' },
{ title: 'White', value: '#ffffff' },
{ title: 'Black', value: '#000000' },
],
instructions: false,
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/autocomplete-multiselect.gif?raw=true" alt="autocompleteMultiselect prompt" width="500" />

### `confirm`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.confirm({
message: 'Confirm?',
name: 'value',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/confirm.gif?raw=true" alt="confirm prompt" width="500" />

### `date`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.date({
message: 'Enter a date',
name: 'date',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/date.gif?raw=true" alt="date prompt" width="500" />

### `invisible`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.invisible({
message: 'Enter password',
name: 'password',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/invisible.gif?raw=true" alt="invisible prompt" width="500" />

### `list`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.list({
message: 'Enter values',
name: 'values',
separator: ' ',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/list.gif?raw=true" alt="list prompt" width="500" />

### `multiselect`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.multiselect({
message: 'Pick colors',
name: 'colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#008000' },
{ title: 'Blue', value: '#0000ff' },
{ title: 'White', value: '#ffffff' },
{ title: 'Black', value: '#000000' },
],
instructions: false,
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/multiselect.gif?raw=true" alt="multiselect prompt" width="500" />

### `number`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.number({
message: 'Enter a number between 10 and 100',
name: 'number',
min: 10,
max: 100,
initial: 0,
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/number.gif?raw=true" alt="number prompt" width="500" />

### `password`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.password({
message: 'Enter password',
name: 'password',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/password.gif?raw=true" alt="password prompt" width="500" />

### `select`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.select({
message: 'Pick a color',
name: 'color',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#008000' },
{ title: 'Blue', value: '#0000ff' },
{ title: 'White', value: '#ffffff' },
{ title: 'Black', value: '#000000' },
],
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/select.gif?raw=true" alt="select prompt" width="500" />

### `text`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.text({
message: 'What is your name?',
name: 'value',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/text.gif?raw=true" alt="text prompt" width="500" />

### `toggle`

```typescript
import $_ from '@lexjs/prompts';

const result = await $_.toggle({
message: 'Confirm?',
name: 'value',
});
```

<img src="https://github.com/LexBorisoff/prompts/blob/main/demo/toggle.gif?raw=true" alt="toggle prompt" width="500" />

## Credits

- `autocomplete`
- `autocompleteMultiselect`
- `confirm`
- `date`
- `invisible`
- `list`
- `multiselect`
- `number`
- `password`
- `select`
- `text`
- `toggle`
This library is a wrapper around the [prompts](https://www.npmjs.com/package/prompts) package.
Binary file added demo/autocomplete-multiselect.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/autocomplete.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/confirm.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/date.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/invisible.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/list.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/multiselect.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/number.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/password.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/select.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/text.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/toggle.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 7 additions & 19 deletions lib/elements/autocomplete.js
Original file line number Diff line number Diff line change
@@ -2,12 +2,13 @@

const color = require('kleur');
const Prompt = require('./prompt');
const { erase, cursor } = require('sisteransi');
const { cursor } = require('sisteransi');
const { style, clear, figures, wrap, entriesToDisplay } = require('../util');

const getVal = (arr, i) => arr[i] && (arr[i].value || arr[i].title || arr[i]);
const getTitle = (arr, i) => arr[i] && (arr[i].title || arr[i].value || arr[i]);
const getIndex = (arr, valOrTitle) => {
if (valOrTitle == null) return undefined;
const index = arr.findIndex(
(el) => el.value === valOrTitle || el.title === valOrTitle,
);
@@ -23,7 +24,6 @@ const getIndex = (arr, valOrTitle) => {
* @param {Number} [opts.limit=10] Max number of results to show
* @param {Number} [opts.cursor=0] Cursor start position
* @param {String} [opts.style='default'] Render style
* @param {String} [opts.fallback] Fallback message - initial to default value
* @param {String} [opts.initial] Index of the default value
* @param {Boolean} [opts.clearFirst] The first ESCAPE keypress will clear the input
* @param {Stream} [opts.stdin] The Readable stream to listen to
@@ -42,7 +42,6 @@ class AutocompletePrompt extends Prompt {
: getIndex(opts.choices, opts.initial);
this.select = this.initial || opts.cursor || 0;
this.i18n = { noMatches: opts.noMatches || 'no matches found' };
this.fallback = opts.fallback || this.initial;
this.clearFirst = opts.clearFirst || false;
this.suggestions = [];
this.input = '';
@@ -57,21 +56,10 @@ class AutocompletePrompt extends Prompt {
this.render();
}

set fallback(fb) {
this._fb = Number.isSafeInteger(parseInt(fb)) ? parseInt(fb) : fb;
}

get fallback() {
let choice;
if (typeof this._fb === 'number') choice = this.choices[this._fb];
else if (typeof this._fb === 'string') choice = { title: this._fb };
return choice || this._fb || { title: this.i18n.noMatches };
}

moveSelect(i) {
this.select = i;
if (this.suggestions.length > 0) this.value = getVal(this.suggestions, i);
else this.value = this.fallback.value;
else this.value = undefined;
this.fire();
}

@@ -240,7 +228,6 @@ class AutocompletePrompt extends Prompt {
render() {
if (this.closed) return;
if (this.firstRender) this.out.write(cursor.hide);
else this.out.write(clear(this.outputText, this.out.columns));
super.render();

let { startIndex, endIndex } = entriesToDisplay(
@@ -252,7 +239,7 @@ class AutocompletePrompt extends Prompt {
this.outputText = [
style.symbol(this.done, this.aborted, this.exited),
color.bold(this.msg),
style.delimiter(this.completing),
style.delimiter(this.done),
this.done && this.suggestions[this.select]
? this.suggestions[this.select].title
: (this.rendered = this.transform.render(this.input)),
@@ -271,10 +258,11 @@ class AutocompletePrompt extends Prompt {
)
.join('\n');
this.outputText +=
`\n` + (suggestions || color.gray(this.fallback.title));
`\n` + (suggestions || color.gray(this.i18n.noMatches));
}

this.out.write(erase.line + cursor.to(0) + this.outputText);
this.out.write(this.clear + this.outputText);
this.clear = clear(this.outputText, this.out.columns);
}
}

12 changes: 7 additions & 5 deletions lib/elements/autocompleteMultiselect.js
Original file line number Diff line number Diff line change
@@ -137,15 +137,17 @@ Instructions:
${figures.arrowUp}/${figures.arrowDown}: Highlight option
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection
[a,b,c]/delete: Filter choices
enter/return: Complete answer
`;
enter/return: Complete answer\n`;
}
return '';
}

renderCurrentInput() {
return `
Filtered results for: ${this.inputValue ? this.inputValue : color.gray('Enter something to filter')}\n`;
return `\nFiltered results for: ${
this.inputValue
? this.inputValue
: color.gray('Enter something to filter')
}\n`;
}

renderOption(cursor, v, i, arrowIndicator) {
@@ -213,7 +215,7 @@ Filtered results for: ${this.inputValue ? this.inputValue : color.gray('Enter so
let prompt = [
style.symbol(this.done, this.aborted),
color.bold(this.msg),
style.delimiter(false),
style.delimiter(this.done),
this.renderDoneOrInstructions(),
].join(' ');

7 changes: 4 additions & 3 deletions lib/elements/confirm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const color = require('kleur');
const Prompt = require('./prompt');
const { style, clear } = require('../util');
const { erase, cursor } = require('sisteransi');
const { cursor } = require('sisteransi');

/**
* ConfirmPrompt Base Element
@@ -25,6 +25,7 @@ class ConfirmPrompt extends Prompt {
this.yesOption = opts.yesOption || '(Y/n)';
this.noMsg = opts.no || 'no';
this.noOption = opts.noOption || '(y/N)';
this.clear = clear('', this.out.columns);
this.render();
}

@@ -71,7 +72,6 @@ class ConfirmPrompt extends Prompt {
render() {
if (this.closed) return;
if (this.firstRender) this.out.write(cursor.hide);
else this.out.write(clear(this.outputText, this.out.columns));
super.render();

this.outputText = [
@@ -85,7 +85,8 @@ class ConfirmPrompt extends Prompt {
: color.gray(this.initialValue ? this.yesOption : this.noOption),
].join(' ');

this.out.write(erase.line + cursor.to(0) + this.outputText);
this.out.write(this.clear + this.outputText);
this.clear = clear(this.outputText, this.out.columns);
}
}

8 changes: 4 additions & 4 deletions lib/elements/date.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
const color = require('kleur');
const Prompt = require('./prompt');
const { style, clear, figures } = require('../util');
const { erase, cursor } = require('sisteransi');
const { cursor } = require('sisteransi');
const {
DatePart,
Meridiem,
@@ -208,14 +208,13 @@ class DatePrompt extends Prompt {
render() {
if (this.closed) return;
if (this.firstRender) this.out.write(cursor.hide);
else this.out.write(clear(this.outputText, this.out.columns));
super.render();

// Print prompt
this.outputText = [
style.symbol(this.done, this.aborted),
color.bold(this.msg),
style.delimiter(false),
style.delimiter(this.done),
this.parts
.reduce(
(arr, p, idx) =>
@@ -240,7 +239,8 @@ class DatePrompt extends Prompt {
);
}

this.out.write(erase.line + cursor.to(0) + this.outputText);
this.out.write(this.clear + this.outputText);
this.clear = clear(this.outputText, this.out.columns);
}
}

39 changes: 21 additions & 18 deletions lib/elements/multiselect.js
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ const { clear, figures, style, wrap, entriesToDisplay } = require('../util');
class MultiselectPrompt extends Prompt {
constructor(opts = {}) {
super(opts);
this.overrideRender = opts.overrideRender;
this.msg = opts.message;
this.cursor = opts.cursor || 0;
this.scrollIndex = opts.cursor || 0;
@@ -178,7 +179,7 @@ class MultiselectPrompt extends Prompt {
` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n` +
` ${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection\n` +
(this.maxChoices === undefined ? ` a: Toggle all\n` : '') +
` enter/return: Complete answer`
` enter/return: Complete answer\n`
);
}
return '';
@@ -221,7 +222,7 @@ class MultiselectPrompt extends Prompt {
// shared with autocompleteMultiselect
paginateOptions(options, addNewLine = true) {
if (options.length === 0) {
return color.red('No matches for this query.');
return color.gray('no matches found');
}

let { startIndex, endIndex } = entriesToDisplay(
@@ -275,23 +276,25 @@ class MultiselectPrompt extends Prompt {
if (this.firstRender) this.out.write(cursor.hide);
super.render();

// print prompt
let prompt = [
style.symbol(this.done, this.aborted),
color.bold(this.msg),
style.delimiter(false),
this.renderDoneOrInstructions(),
].join(' ');
if (this.showMinError) {
prompt += color.red(
`You must select a minimum of ${this.minSelected} choices.`,
);
this.showMinError = false;
}
prompt += this.renderOptions(this.value);
if (!this.overrideRender) {
// print prompt
let prompt = [
style.symbol(this.done, this.aborted),
color.bold(this.msg),
style.delimiter(this.done),
this.renderDoneOrInstructions(),
].join(' ');
if (this.showMinError) {
prompt += color.red(
`You must select a minimum of ${this.minSelected} choices.`,
);
this.showMinError = false;
}
prompt += this.renderOptions(this.value);

this.out.write(this.clear + prompt);
this.clear = clear(prompt, this.out.columns);
this.out.write(this.clear + prompt);
this.clear = clear(prompt, this.out.columns);
}
}
}

19 changes: 9 additions & 10 deletions lib/elements/number.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const color = require('kleur');
const Prompt = require('./prompt');
const { cursor, erase } = require('sisteransi');
const { cursor } = require('sisteransi');
const { style, figures, clear, lines } = require('../util');

const isNumber = /[0-9]/;
@@ -43,6 +43,7 @@ class NumberPrompt extends Prompt {
this.value = ``;
this.typed = ``;
this.lastHit = 0;
this.clear = clear('', this.out.columns);
this.render();
}

@@ -183,13 +184,11 @@ class NumberPrompt extends Prompt {

render() {
if (this.closed) return;
if (!this.firstRender) {
if (this.outputError)
this.out.write(
cursor.down(lines(this.outputError, this.out.columns) - 1) +
clear(this.outputError, this.out.columns),
);
this.out.write(clear(this.outputText, this.out.columns));
if (!this.firstRender && this.outputError) {
this.out.write(
cursor.down(lines(this.outputError, this.out.columns) - 1) +
clear(this.outputError, this.out.columns),
);
}
super.render();
this.outputError = '';
@@ -216,13 +215,13 @@ class NumberPrompt extends Prompt {
}

this.out.write(
erase.line +
cursor.to(0) +
this.clear +
this.outputText +
cursor.save +
this.outputError +
cursor.restore,
);
this.clear = clear(this.outputText, this.out.columns);
}
}

8 changes: 4 additions & 4 deletions lib/elements/select.js
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ class SelectPrompt extends Prompt {
constructor(opts = {}) {
super(opts);
this.msg = opts.message;
this.hint = opts.hint || '- Use arrow-keys. Return to submit.';
this.hint = opts.hint || '';
this.warn = opts.warn || '- This option is disabled';
this.cursor = opts.initial || 0;
this.choices = opts.choices.map((ch, idx) => {
@@ -118,7 +118,6 @@ class SelectPrompt extends Prompt {
render() {
if (this.closed) return;
if (this.firstRender) this.out.write(cursor.hide);
else this.out.write(clear(this.outputText, this.out.columns));
super.render();

let { startIndex, endIndex } = entriesToDisplay(
@@ -131,7 +130,7 @@ class SelectPrompt extends Prompt {
this.outputText = [
style.symbol(this.done, this.aborted),
color.bold(this.msg),
style.delimiter(false),
style.delimiter(this.done),
this.done
? this.selection.title
: this.selection.disabled
@@ -188,7 +187,8 @@ class SelectPrompt extends Prompt {
}
}

this.out.write(this.outputText);
this.out.write(this.clear + this.outputText);
this.clear = clear(this.outputText, this.out.columns);
}
}

18 changes: 8 additions & 10 deletions lib/elements/text.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const color = require('kleur');
const Prompt = require('./prompt');
const { erase, cursor } = require('sisteransi');
const { cursor } = require('sisteransi');
const { style, clear, lines, figures } = require('../util');

/**
@@ -186,13 +186,11 @@ class TextPrompt extends Prompt {

render() {
if (this.closed) return;
if (!this.firstRender) {
if (this.outputError)
this.out.write(
cursor.down(lines(this.outputError, this.out.columns) - 1) +
clear(this.outputError, this.out.columns),
);
this.out.write(clear(this.outputText, this.out.columns));
if (!this.firstRender && this.outputError) {
this.out.write(
cursor.down(lines(this.outputError, this.out.columns) - 1) +
clear(this.outputError, this.out.columns),
);
}
super.render();
this.outputError = '';
@@ -215,14 +213,14 @@ class TextPrompt extends Prompt {
}

this.out.write(
erase.line +
cursor.to(0) +
this.clear +
this.outputText +
cursor.save +
this.outputError +
cursor.restore +
cursor.move(this.cursorOffset, 0),
);
this.clear = clear(this.outputText, this.out.columns);
}
}

7 changes: 4 additions & 3 deletions lib/elements/toggle.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const color = require('kleur');
const Prompt = require('./prompt');
const { style, clear } = require('../util');
const { cursor, erase } = require('sisteransi');
const { cursor } = require('sisteransi');

/**
* TogglePrompt Base Element
@@ -21,6 +21,7 @@ class TogglePrompt extends Prompt {
this.active = opts.active || 'on';
this.inactive = opts.inactive || 'off';
this.initialValue = this.value;
this.clear = clear('', this.out.columns);
this.render();
}

@@ -99,7 +100,6 @@ class TogglePrompt extends Prompt {
render() {
if (this.closed) return;
if (this.firstRender) this.out.write(cursor.hide);
else this.out.write(clear(this.outputText, this.out.columns));
super.render();

this.outputText = [
@@ -111,7 +111,8 @@ class TogglePrompt extends Prompt {
this.value ? color.cyan().underline(this.active) : this.active,
].join(' ');

this.out.write(erase.line + cursor.to(0) + this.outputText);
this.out.write(this.clear + this.outputText);
this.clear = clear(this.outputText, this.out.columns);
}
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@lexjs/prompts",
"version": "0.0.0-semantically-released",
"description": "Prompts",
"description": "Interactive prompts",
"publishConfig": {
"access": "public",
"provenance": true

0 comments on commit b5a1fc9

Please sign in to comment.