inquirer-reactive-list-prompt
v1.1.0
Published
inquirer prompt for reactive choices on list
Maintainers
Readme
Features
- Reactive choices via
BehaviorSubject— update the list in real-time - Loading spinner via ora with customizable start/stop options
- Markdown description panel rendered in terminal via marked-terminal
- Paginated description with PgUp/PgDn/Home/End navigation
- Error and disabled choice states with visual distinction
- ESC key support for graceful cancellation
- Infinite or finite list scrolling
Install
npm install inquirer-reactive-list-prompt [email protected] rxjsPeer dependency: requires
[email protected](exact version).
Quick Start
import inquirer from 'inquirer';
import ReactiveListPrompt, { ChoiceItem, ReactiveListLoader } from 'inquirer-reactive-list-prompt';
import { BehaviorSubject } from 'rxjs';
const choices$ = new BehaviorSubject<ChoiceItem[]>([]);
const loader$ = new BehaviorSubject<ReactiveListLoader>({
isLoading: false,
startOption: { text: 'Loading...' },
});
inquirer.registerPrompt('reactiveListPrompt', ReactiveListPrompt);
const answer = await inquirer.prompt({
type: 'reactiveListPrompt',
name: 'selection',
message: 'Pick an item:',
emptyMessage: '⚠ No items yet',
choices$,
loader$,
});
// Push choices asynchronously
loader$.next({ isLoading: true });
setTimeout(() => {
choices$.next([
{ name: 'Option A', value: 'a' },
{ name: 'Option B', value: 'b', description: '## Details\nThis is **markdown**' },
{ name: 'Failed', value: 'err', disabled: true, isError: true },
]);
loader$.next({ isLoading: false, message: 'Done' });
}, 2000);
console.log('Selected:', answer.selection);Prompt Options
| Option | Type | Default | Description |
| ---------------------- | ------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
| type | 'reactiveListPrompt' | required | Prompt type identifier |
| name | string | required | Answer key in the returned object |
| message | string | required | Prompt message displayed to the user |
| choices$ | BehaviorSubject<ChoiceItem[]> | — | Reactive stream of choices. Push new arrays with .next() to update the list dynamically |
| loader$ | BehaviorSubject<ReactiveListLoader> | — | Reactive stream for loading spinner state |
| emptyMessage | string | 'No choices' | Message shown when the choice list is empty |
| pageSize | number | 7 | Number of visible choices before scrolling |
| loop | boolean | true | Enable infinite scrolling (wraps around) |
| showDescription | boolean | false | Show markdown description panel for the selected choice |
| descPageSize | number | 7 | Number of visible lines in the description panel |
| pickKey | 'short' \| 'value' | 'value' | Which field to display after selection |
| defaultPickMessage | string | 'Selected' | Fallback display text when pickKey field is missing |
| isDescriptionDim | boolean | false | Render description in dim (muted) style |
| paginatorInstruction | string | '(Use ↑↓ arrows to move)' | Custom navigation instruction text |
| allowEscapeOnChoices | boolean | false | Allow ESC key to cancel even when choices exist. By default, ESC only works when the list is empty |
Types
ChoiceItem / ReactiveListChoice
type ReactiveListChoice = {
name?: string; // Display text
value: string; // Value returned on selection
short?: string; // Short display text (shown after selection when pickKey='short')
description?: string; // Markdown content for the description panel
disabled?: boolean | string; // Disable with optional reason text
isError?: boolean; // Render as error state (red cross icon)
id?: string; // Optional identifier
checked?: boolean; // Optional checked state
};ReactiveListLoader
interface ReactiveListLoader {
isLoading: boolean; // true = show spinner, false = show done state
message?: string; // Text shown when loading stops
startOption?: OraOptions; // ora spinner options (color, spinner, text)
stopOption?: StopSpinnerOption; // Customize the done frame and color
clear?: boolean; // Clear the spinner entirely
}StopSpinnerOption
interface StopSpinnerOption {
doneFrame?: string; // Character shown when stopped (default: '✔')
color?: 'black' | 'green' | 'red' | 'yellow' | 'blue' | 'cyan' | 'white';
}Keyboard Navigation
| Key | Action |
| --------------- | --------------------------------------------------------------------------------------------------- |
| ↑ / ↓ | Move selection up/down |
| Enter | Confirm selection |
| 0-9 | Jump to choice by number |
| PgUp / PgDn | Scroll description page up/down |
| Home / End | Jump to first/last description page |
| ESC | Cancel prompt (returns null). Works when list is empty, or always if allowEscapeOnChoices: true |
Usage Patterns
Streaming Choices
Push choices incrementally as they become available:
// Initial empty state
choices$.next([]);
loader$.next({ isLoading: true });
// First result arrives
choices$.next([
{ name: 'Result 1', value: 'r1' },
{ name: 'Still loading...', value: 'pending', disabled: true },
]);
// More results
choices$.next([
{ name: 'Result 1', value: 'r1' },
{ name: 'Result 2', value: 'r2' },
{ name: 'Result 3', value: 'r3' },
]);
// Loading complete
loader$.next({
isLoading: false,
message: 'All results loaded',
stopOption: { doneFrame: '✔', color: 'green' },
});Error Handling
Show errors inline as disabled choices:
choices$.next([
{ name: 'Success result', value: 'ok' },
{ name: 'API timeout', value: 'err1', disabled: true, isError: true },
{ name: 'Rate limited', value: 'err2', disabled: true, isError: true },
]);Markdown Description
Enable the description panel to show rich content:
const answer = await inquirer.prompt({
type: 'reactiveListPrompt',
name: 'commit',
message: 'Select commit message:',
showDescription: true,
descPageSize: 10,
choices$,
loader$,
});
choices$.next([
{
name: 'feat: add authentication',
value: 'feat-auth',
short: 'feat: add auth',
description: `## Changes\n- Added JWT middleware\n- Created login endpoint\n\n\`\`\`typescript\nconst token = jwt.sign(payload, secret);\n\`\`\``,
},
]);Programmatic Control
Access the prompt instance for programmatic abort:
const prompt = inquirer.prompt({ type: 'reactiveListPrompt', ... });
// Abort from external signal (e.g., timeout)
setTimeout(() => {
prompt.ui.activePrompt.abortPrompt(); // Resolves with null
}, 30000);Example
See the example/ directory for runnable demos:
# Run the main example (reactive choices + loader + description)
pnpm run pack:example
# Or manually:
cd example
pnpm install
pnpm start # Main demo
pnpm run no-response # ESC key / timeout scenarios
pnpm run escape-demo # allowEscapeOnChoices demoStay in touch
License
MIT
