Skip to content

Lightweight prompting library for terminal apps.

License

Notifications You must be signed in to change notification settings

fabiospampinato/prask

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Prask

Lightweight prompting library for terminal apps.

Install

npm install --save prask

Prompts

Input Selection Others
string multiselect log
invisible select prompt
password boolean spinner
number toggle
rating

string

Demo

This prompt can be used to ask the user for a string value.

import {string} from 'prask';
import color from 'tiny-colors';

const result = await string ({
  /* REQUIRED OPTIONS */
  message: 'What is your name?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: 'John Doe', // Optional default value, to allow the user to quickly press Enter for it
  required: true, // Whether a non-empty value for this prompt is required or not
  format: ( value, settled ) => value.length >= 2 ? colors.green ( value ) : colors.red ( value ), // Optional formatting function, the visual length of the output string must remain the same
  validate: value => value.length >= 2 // Optional validation function
});

Interactions:

Trigger Description
Esc Quit the prompt, which will resolve to undefined.
Enter Submit the value, or the initial value if visible, checking if it's valid first.
Tab Edit the initial value, if visible.

invisible

Demo

This prompt can be used to ask the user for a password, without visually leaking even the length of the password to the console.

import {invisible} from 'prask';

const result = await invisible ({
  /* REQUIRED OPTIONS */
  message: 'What is your password?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: 'P@assword!', // Optional default value, to allow the user to quickly press Enter for it, better not to use it since it will be visible
  required: true, // Whether a non-empty value for this prompt is required or not
  validate: value => value.length >= 8 // Optional validation function
});

It can be interacted with exactly like a string prompt, the only difference is that the value is invisible.

password

Demo

This prompt can be used to ask the user for a password, without visually leaking the characters that make it up to the console.

import {password} from 'prask';

const result = await password ({
  /* REQUIRED OPTIONS */
  message: 'What is your password?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: 'P@assword!', // Optional default value, to allow the user to quickly press Enter for it, better not to use it since it will be visible
  required: true, // Whether a non-empty value for this prompt is required or not
  validate: value => value.length >= 8 // Optional validation function
});

It can be interacted with exactly like a string prompt, the only difference is that every character in the value will be replaced with an asterisk.

number

Demo

This prompt can be used to ask the user for a numeric value.

import {number} from 'prask';
import colors from 'tiny-colors';

const result = await number ({
  /* REQUIRED OPTIONS */
  message: 'What is your favorite even number?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: 42, // Optional default value, to allow the user to quickly press Enter for it
  format: ( value, settled ) => ( value % 2 ) ? colors.red ( value ) : colors.green ( value ), // Optional formatting function, the visual length of the output string must remain the same
  validate: value => ( value % 2 ) ? 'The number must be even' : true // Optional validation function
});

It can be interacted with exactly like a string prompt, the main difference is that the value of the input is also automatically validated to be a number, plus the following extra interactions are supported:

Trigger Description
Up/Right Increment the current value by 1.
Down/Left Decrement the current value by 1.

multiselect

Demo

This prompt can be used to ask the user to pick from zero to many options between the provided ones.

import {multiselect} from 'prask';

const result = await multiselect ({
  /* REQUIRED OPTIONS */
  message: 'Which countries would you like to visit?', // The message that the user will read
  options: [
    { title: 'Italy', value: 'it' },
    { title: 'Turkey', value: 'tr' },
    { title: 'United Kingdom', value: 'uk' }
  ],
  /* OPTIONAL OPTIONS */
  limit: 10, // Limit to this number the maximum number of options visible at one time
  min: 1, // Require at least this number of options to be selected
  max: 10, // Require at most this number of options to be selected
  searchable: false, // Turn off support for filtering the list of options
  validate: values => ( values.length % 2 ) // Optional validation function
});

Each option must have the following shape:

type Option<T> = {
  /* REQUIRED VALUES */
  title: string, // The name of the option that will be showed to the user
  value: T, // The value of the option that the prompt will return you when this option is selected
  /* OPTIONAL VALUES */
  disabled?: boolean, // Whether this option can be toggled or not
  description?: string, // The description that will be showed next to the title
  heading?: boolean, // Whether this option is an unselectable heading or not
  hint?: string, // The description that will be showed next to the title, only when the item is focused
  selected?: boolean // Whether this option is pre-selected or not
};

Options can be provided dynamically also, with a function that returns an array of options depending on the current query. That can also be used to customize how results are searched and ranked.

Interactions:

Trigger Description
Esc Quit the prompt, which will resolve to undefined.
Enter Submit the selected values, checking if the selection is valid first.
Space Toggle the focused option.
Up Move the focus to the previous item, and potentially scroll to the previous page.
Down Move the focus to the next item, and potentially scroll to the next page.
* All other keys will be used to edit the search query, which will filter down the list.

select

Demo

This prompt can be used to ask the user to pick one option between the provided ones.

import {select} from 'prask';

const result = await select ({
  /* REQUIRED OPTIONS */
  message: 'Which country would you like to visit first?', // The message that the user will read
  options: [
    { title: 'Italy', value: 'it' },
    { title: 'Turkey', value: 'tr' },
    { title: 'United Kingdom', value: 'uk' }
  ],
  /* OPTIONAL OPTIONS */
  limit: 10, // Limit to this number the maximum number of options visible at one time
  searchable: false, // Turn off support for filtering the list of options
  validate: value => value !== 'Foo' // Optional validation function
});

It can be interacted with exactly like a multiselect prompt.

boolean

Demo

This prompt can be used to ask the user to pick between yes and no.

import {boolean} from 'prask';

const result = await boolean ({
  /* REQUIRED OPTIONS */
  message: 'Do you like this library?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: true // Select this option by default
});

It can be interacted with exactly like a select prompt, except that search is turned off for it.

toggle

Demo

This prompt can be used to ask the user to pick between yes and no, using a single line of output, rather than the three that boolean needs.

import {toggle} from 'prask';

const result = await toggle ({
  /* REQUIRED OPTIONS */
  message: 'Do you like this library?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: true // Select this option by default
});

Interactions:

Trigger Description
Esc Quit the prompt, which will resolve to undefined.
Enter Submit the selected option.
Left/Right/Up/Down/Tab Select the other option.

rating

Demo

This prompt can be used to ask the user for a numeric rating between 1 and 5.

import {rating} from 'prask';

const result = await rating ({
  /* REQUIRED OPTIONS */
  message: 'How would you rate this library?', // The message that the user will read
  /* OPTIONAL OPTIONS */
  initial: 5 // Select this option by default
});

Interactions:

Trigger Description
Esc Quit the prompt, which will resolve to undefined.
Enter Submit the selected rating.
Right/Up Increment the selected rating.
Left/Down Decrement the selected rating.

log

Demo

This utility provides functions for logging a line of text to the console, with the same style that the built-in prompts use.

import {log} from 'prask';

log.success ( 'Success message' );
log.error ( 'Error message' );
log.warning ( 'Warning message' );
log.question ( 'Question message' );
log.info ( 'Info message' );

prompt

This is a low-level general prompt, which is used internally to implement the other higher-level prompts, and which you can use to implement your own custom prompts.

import {prompt} from 'prask';
import color from 'tiny-colors';

// Let's define the "rating" prompt, it returns a number between 1 and 5, which is the rating the user picked
// This is the actual internal implementation for that component, so you can see exactly how it works

type Rating = 1 | 2 | 3 | 4 | 5;

type Options = {
  message: string,
  initial?: Rating
};

// A prompt could always resolve to "undefined", if the user escapes from it
const rating = ( options: Options ): Promise<Rating | undefined> => {

  // Let's define some internal constants we need for rendering

  const STAR_ACTIVE = color.green ( '●' );
  const STAR_INACTIVE = '○';
  const STARS_DIVIDER = '─────';
  const LABELS_DIVIDER = '     ';

  // Let's define some internal state variables
  // The status variable let's us keep track of whether this prompt was escaped (-1), submitted (1), or it's still pending (0)

  let {message, initial = 3} = options;
  let status: -1 | 0 | 1 = 0;
  let current = initial;

  // Let's define some components
  // Basically each prompt re-renders its components in a loop, React-style
  // Each prompt must return something to render, which can be a single component or an array of components
  // A component is just a function that can either return a string, which will be printed to the console, or undefined, which will be ignored
  // The "prompt" function takes care of cleaning up the previous output and replacing it with the new one
  // That's basically how the entire library works, the "prompt" function tells you when a key is pressed, and you just tell it what to render next

  const getStatusSymbol = ( status ): string => {
    if ( status < 0 ) return color.red ( '✖' );
    if ( status > 0 ) return color.green ( '✔' );
    return color.cyan.bold ( '?' );
  };

  const question = (): string => {
    const question = `${getStatusSymbol ()} ${color.bold ( message )}`;
    const result = status === 1 ? color.cyan ( String ( current ) ) : '';
    return [question, result].join ( ' ' );
  };

  const stars = (): string => {
    const star1 = ( current === 1 ) ? STAR_ACTIVE : STAR_INACTIVE;
    const star2 = ( current === 2 ) ? STAR_ACTIVE : STAR_INACTIVE;
    const star3 = ( current === 3 ) ? STAR_ACTIVE : STAR_INACTIVE;
    const star4 = ( current === 4 ) ? STAR_ACTIVE : STAR_INACTIVE;
    const star5 = ( current === 5 ) ? STAR_ACTIVE : STAR_INACTIVE;
    const stars = [star1, star2, star3, star4, star5].join ( STARS_DIVIDER );
    return stars;
  };

  const labels = (): string => {
    return [1, 2, 3, 4, 5].join ( LABELS_DIVIDER );
  };

  // Now let's handle keypresses

  return prompt ( ( resolve, {key} ) => {
    if ( key === '' ) { // Initial render, no key has been pressed yet
      return [question, stars, labels];
    } else if ( key === 'escape' ) { // Escape, let's bail out
      status = -1;
      resolve ( undefined );
      return [question];
    } else if ( key === 'return' ) { // Enter, let's return the current rating
      status = 1;
      resolve ( current );
      return [question];
    } else if ( key === 'left' || key === 'down' ) { // Left or Down, let's decrement the rating, if possible
      current = Math.max ( 1, current - 1 );
      return [question, stars, labels];
    } else if ( key === 'right' || key === 'up' ) { // Right or Up, let's increment the rating, if possible
      current = Math.min ( 5, current + 1 );
      return [question, stars, labels];
    } else { // Something else was pressed, let's just ignore it
      return [question, stars, labels];
    }
  });

};

spinner

Demo

The spinner prompt is a convenience wrapper around tiny-spinner, which you could also use directly if you prefer.

It allows to notify the user of in progress operations, and can resolve to a successful or an unsuccessful state.

import {spinner} from 'prompts';

const result = await spinner ( async ({ update, resolve, reject }) => {
  update ( 'Working' );
  await delay ( 750 );
  update ( 'Working hard' );
  await delay ( 750 );
  update ( 'Working harder' );
  await delay ( 750 );
  update ( 'Working real hard' );
  await delay ( 750 );
  let result = true; // Let's say this is the result of the operation
  if ( result ) {
    resolve ( 'Work succeeded' );
  } else {
    reject ( 'Work failed' );
  }
});

License

MIT © Fabio Spampinato