File size: 11,844 Bytes
d4b85c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# `@inquirer/core`

The `@inquirer/core` package is the library enabling the creation of Inquirer prompts.

It aims to implements a lightweight API similar to React hooks - but without JSX.

# Special Thanks

<div align="center" markdown="1">

[![Graphite](https://github.com/user-attachments/assets/53db40ca-2254-481a-a094-6597f8716e29)](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)<br>

### [Graphite is the AI developer productivity platform helping teams on GitHub ship higher quality software, faster](https://graphite.dev/?utm_source=npmjs&utm_medium=repo&utm_campaign=inquirerjs)

</div>

# Installation

<table>
<tr>
  <th>npm</th>
  <th>yarn</th>
</tr>
<tr>
<td>

```sh
npm install @inquirer/core
```

</td>
<td>

```sh
yarn add @inquirer/core
```

</td>
</tr>
</table>

# Usage

## Basic concept

Visual terminal apps are at their core strings rendered onto the terminal.

The most basic prompt is a function returning a string that'll be rendered in the terminal. This function will run every time the prompt state change, and the new returned string will replace the previously rendered one. The prompt cursor appears after the string.

Wrapping the rendering function with `createPrompt()` will setup the rendering layer, inject the state management utilities, and wait until the `done` callback is called.

```ts
import { createPrompt } from '@inquirer/core';

const input = createPrompt((config, done) => {
  // Implement logic

  return '? My question';
});

// And it is then called as
const answer = await input({
  /* config */
});
```

## Hooks

State management and user interactions are handled through hooks. Hooks are common [within the React ecosystem](https://react.dev/reference/react/hooks), and Inquirer reimplement the common ones.

### State hook

State lets a component “remember” information like user input. For example, an input prompt can use state to store the input value, while a list prompt can use state to track the cursor index.

`useState` declares a state variable that you can update directly.

```ts
import { createPrompt, useState } from '@inquirer/core';

const input = createPrompt((config, done) => {
  const [index, setIndex] = useState(0);

  // ...
```

### Keypress hook

Almost all prompts need to react to user actions. In a terminal, this is done through typing.

`useKeypress` allows you to react to keypress events, and access the prompt line.

```ts
const input = createPrompt((config, done) => {
  useKeypress((key) => {
    if (key.name === 'enter') {
      done(answer);
    }
  });

  // ...
```

Behind the scenes, Inquirer prompts are wrappers around [readlines](https://nodejs.org/api/readline.html). Aside the keypress event object, the hook also pass the active readline instance to the event handler.

```ts
const input = createPrompt((config, done) => {
  useKeypress((key, readline) => {
    setValue(readline.line);
  });

  // ...
```

### Ref hook

Refs let a prompt hold some information that isn’t used for rendering, like a class instance or a timeout ID. Unlike with state, updating a ref does not re-render your prompt. Refs are an “escape hatch” from the rendering paradigm.

`useRef` declares a ref. You can hold any value in it, but most often it’s used to hold a timeout ID.

```ts
const input = createPrompt((config, done) => {
  const timeout = useRef(null);

  // ...
```

### Effect Hook

Effects let a prompt connect to and synchronize with external systems. This includes dealing with network or animations.

`useEffect` connects a component to an external system.

```ts
const chat = createPrompt((config, done) => {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  // ...
```

### Performance hook

A common way to optimize re-rendering performance is to skip unnecessary work. For example, you can tell Inquirer to reuse a cached calculation or to skip a re-render if the data has not changed since the previous render.

`useMemo` lets you cache the result of an expensive calculation.

```ts
const todoSelect = createPrompt((config, done) => {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);

  // ...
```

### Rendering hooks

#### Prefix / loading

All default prompts, and most custom ones, uses a prefix at the beginning of the prompt line. This helps visually delineate different questions, and provides a convenient area to render a loading spinner.

`usePrefix` is a built-in hook to do this.

```ts
const input = createPrompt((config, done) => {
  const prefix = usePrefix({ status });

  return `${prefix} My question`;
});
```

#### Pagination

When looping through a long list of options (like in the `select` prompt), paginating the results appearing on the screen at once can be necessary. The `usePagination` hook is the utility used within the `select` and `checkbox` prompts to cycle through the list of options.

Pagination works by taking in the list of options and returning a subset of the rendered items that fit within the page. The hook takes in a few options. It needs a list of options (`items`), and a `pageSize` which is the number of lines to be rendered. The `active` index is the index of the currently selected/selectable item. The `loop` option is a boolean that indicates if the list should loop around when reaching the end: this is the default behavior. The pagination hook renders items only as necessary, so it takes a function that can render an item at an index, including an `active` state, called `renderItem`.

```js
export default createPrompt((config, done) => {
  const [active, setActive] = useState(0);

  const allChoices = config.choices.map((choice) => choice.name);

  const page = usePagination({
    items: allChoices,
    active: active,
    renderItem: ({ item, index, isActive }) => `${isActive ? ">" : " "}${index}. ${item.toString()}`
    pageSize: config.pageSize,
    loop: config.loop,
  });

  return `... ${page}`;
});
```

## `createPrompt()` API

As we saw earlier, the rendering function should return a string, and eventually call `done` to close the prompt and return the answer.

```ts
const input = createPrompt((config, done) => {
  const [value, setValue] = useState();

  useKeypress((key, readline) => {
    if (key.name === 'enter') {
      done(answer);
    } else {
      setValue(readline.line);
    }
  });

  return `? ${config.message} ${value}`;
});
```

The rendering function can also return a tuple of 2 string (`[string, string]`.) The first string represents the prompt. The second one is content to render under the prompt, like an error message. The text input cursor will appear after the first string.

```ts
const number = createPrompt((config, done) => {
  // Add some logic here

  return [`? My question ${input}`, `! The input must be a number`];
});
```

### Typescript

If using typescript, `createPrompt` takes 2 generic arguments.

```ts
// createPrompt<Value, Config>
const input = createPrompt<string, { message: string }>(// ...
```

The first one is the type of the resolved value

```ts
const answer: string = await input();
```

The second one is the type of the prompt config; in other words the interface the created prompt will provide to users.

```ts
const answer = await input({
  message: 'My question',
});
```

## Key utilities

Listening for keypress events inside an inquirer prompt is a very common pattern. To ease this, we export a few utility functions taking in the keypress event object and return a boolean:

- `isEnterKey()`
- `isBackspaceKey()`
- `isSpaceKey()`
- `isUpKey()` - Note: this utility will handle vim and emacs keybindings (up, `k`, and `ctrl+p`)
- `isDownKey()` - Note: this utility will handle vim and emacs keybindings (down, `j`, and `ctrl+n`)
- `isNumberKey()` one of 1, 2, 3, 4, 5, 6, 7, 8, 9, 0

## Theming

Theming utilities will allow you to expose customization of the prompt style. Inquirer also has a few standard theme values shared across all the official prompts.

To allow standard customization:

```ts
import { createPrompt, usePrefix, makeTheme, type Theme } from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';

type PromptConfig = {
  theme?: PartialDeep<Theme>;
};

export default createPrompt<string, PromptConfig>((config, done) => {
  const theme = makeTheme(config.theme);

  const prefix = usePrefix({ status, theme });

  return `${prefix} ${theme.style.highlight('hello')}`;
});
```

To setup a custom theme:

```ts
import { createPrompt, makeTheme, type Theme } from '@inquirer/core';
import type { PartialDeep } from '@inquirer/type';

type PromptTheme = {};

const promptTheme: PromptTheme = {
  icon: '!',
};

type PromptConfig = {
  theme?: PartialDeep<Theme<PromptTheme>>;
};

export default createPrompt<string, PromptConfig>((config, done) => {
  const theme = makeTheme(promptTheme, config.theme);

  const prefix = usePrefix({ status, theme });

  return `${prefix} ${theme.icon}`;
});
```

The [default theme keys cover](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/core/src/lib/theme.ts):

```ts
type DefaultTheme = {
  prefix: string | { idle: string; done: string };
  spinner: {
    interval: number;
    frames: string[];
  };
  style: {
    answer: (text: string) => string;
    message: (text: string, status: 'idle' | 'done' | 'loading') => string;
    error: (text: string) => string;
    defaultAnswer: (text: string) => string;
    help: (text: string) => string;
    highlight: (text: string) => string;
    key: (text: string) => string;
  };
};
```

# Examples

You can refer to any `@inquirer/prompts` prompts for real examples:

- [Confirm Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/confirm/src/index.ts)
- [Input Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/input/src/index.ts)
- [Password Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/password/src/index.ts)
- [Editor Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/editor/src/index.ts)
- [Select Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/select/src/index.ts)
- [Checkbox Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/checkbox/src/index.ts)
- [Rawlist Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/rawlist/src/index.ts)
- [Expand Prompt](https://github.com/SBoudrias/Inquirer.js/blob/main/packages/expand/src/index.ts)

```ts
import colors from 'yoctocolors';
import {
  createPrompt,
  useState,
  useKeypress,
  isEnterKey,
  usePrefix,
  type Status,
} from '@inquirer/core';

const confirm = createPrompt<boolean, { message: string; default?: boolean }>(
  (config, done) => {
    const [status, setStatus] = useState<Status>('idle');
    const [value, setValue] = useState('');
    const prefix = usePrefix({});

    useKeypress((key, rl) => {
      if (isEnterKey(key)) {
        const answer = value ? /^y(es)?/i.test(value) : config.default !== false;
        setValue(answer ? 'yes' : 'no');
        setStatus('done');
        done(answer);
      } else {
        setValue(rl.line);
      }
    });

    let formattedValue = value;
    let defaultValue = '';
    if (status === 'done') {
      formattedValue = colors.cyan(value);
    } else {
      defaultValue = colors.dim(config.default === false ? ' (y/N)' : ' (Y/n)');
    }

    const message = colors.bold(config.message);
    return `${prefix} ${message}${defaultValue} ${formattedValue}`;
  },
);

/**
 *  Which then can be used like this:
 */
const answer = await confirm({ message: 'Do you want to continue?' });
```

# License

Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>
Licensed under the MIT license.