Spaces:
Running
Running
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">
[](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.
|