Skip to content

Commit 0d9fd4e

Browse files
committed
Feat: Apply theming to all prompts
1 parent e3eefcb commit 0d9fd4e

21 files changed

+351
-112
lines changed

packages/checkbox/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,39 @@ const answer = await checkbox({
4343
| loop | `boolean` | no | Defaults to `true`. When set to `false`, the cursor will be constrained to the top and bottom of the choice list without looping. |
4444
| required | `boolean` | no | When set to `true`, ensures at least one choice must be selected. |
4545
| validate | `string\[\] => boolean \| string \| Promise<string \| boolean>` | no | On submit, validate the choices. When returning a string, it'll be used as the error message displayed to the user. Note: returning a rejected promise, we'll assume a code error happened and crash. |
46+
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
4647

4748
The `Separator` object can be used to render non-selectable lines in the choice list. By default it'll render a line, but you can provide the text as argument (`new Separator('-- Dependencies --')`). This option is often used to add labels to groups within long list of options.
4849

50+
## Theming
51+
52+
You can theme a prompt by passing a `theme` object option. The theme object only need to includes the keys you wish to modify, we'll fallback on the defaults for the rest.
53+
54+
```ts
55+
type Theme = {
56+
prefix: string;
57+
spinner: {
58+
interval: number;
59+
frames: string[];
60+
};
61+
style: {
62+
answer: (text: string) => string;
63+
message: (text: string) => string;
64+
error: (text: string) => string;
65+
defaultAnswer: (text: string) => string;
66+
help: (text: string) => string;
67+
highlight: (text: string) => string;
68+
key: (text: string) => string;
69+
disabledChoice: (text: string) => string;
70+
};
71+
icon: {
72+
checked: string;
73+
unchecked: string;
74+
cursor: string;
75+
};
76+
};
77+
```
78+
4979
# License
5080

5181
Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))<br/>

packages/checkbox/src/index.mts

+53-28
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,42 @@ import {
55
usePrefix,
66
usePagination,
77
useMemo,
8+
makeTheme,
89
isUpKey,
910
isDownKey,
1011
isSpaceKey,
1112
isNumberKey,
1213
isEnterKey,
1314
Separator,
15+
type Theme,
1416
} from '@inquirer/core';
15-
import type {} from '@inquirer/type';
17+
import type { PartialDeep } from '@inquirer/type';
1618
import chalk from 'chalk';
1719
import figures from 'figures';
1820
import ansiEscapes from 'ansi-escapes';
1921

22+
type CheckboxTheme = {
23+
icon: {
24+
checked: string;
25+
unchecked: string;
26+
cursor: string;
27+
};
28+
style: {
29+
disabledChoice: (text: string) => string;
30+
};
31+
};
32+
33+
const checkboxTheme: CheckboxTheme = {
34+
icon: {
35+
checked: chalk.green(figures.circleFilled),
36+
unchecked: figures.circle,
37+
cursor: figures.pointer,
38+
},
39+
style: {
40+
disabledChoice: (text: string) => chalk.dim(`- ${text}`),
41+
},
42+
};
43+
2044
type Choice<Value> = {
2145
name?: string;
2246
value: Value;
@@ -36,6 +60,7 @@ type Config<Value> = {
3660
validate?: (
3761
items: ReadonlyArray<Item<Value>>,
3862
) => boolean | string | Promise<string | boolean>;
63+
theme?: PartialDeep<Theme<CheckboxTheme>>;
3964
};
4065

4166
type Item<Value> = Separator | Choice<Value>;
@@ -58,35 +83,18 @@ function check(checked: boolean) {
5883
};
5984
}
6085

61-
function renderItem<Value>({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
62-
if (Separator.isSeparator(item)) {
63-
return ` ${item.separator}`;
64-
}
65-
66-
const line = item.name || item.value;
67-
if (item.disabled) {
68-
const disabledLabel =
69-
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
70-
return chalk.dim(`- ${line} ${disabledLabel}`);
71-
}
72-
73-
const checkbox = item.checked ? chalk.green(figures.circleFilled) : figures.circle;
74-
const color = isActive ? chalk.cyan : (x: string) => x;
75-
const prefix = isActive ? figures.pointer : ' ';
76-
return color(`${prefix}${checkbox} ${line}`);
77-
}
78-
7986
export default createPrompt(
8087
<Value extends unknown>(config: Config<Value>, done: (value: Array<Value>) => void) => {
8188
const {
82-
prefix = usePrefix(),
8389
instructions,
8490
pageSize = 7,
8591
loop = true,
8692
choices,
8793
required,
8894
validate = () => true,
8995
} = config;
96+
const theme = makeTheme<CheckboxTheme>(checkboxTheme, config.theme);
97+
const prefix = usePrefix({ theme });
9098
const [status, setStatus] = useState('pending');
9199
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
92100
choices.map((choice) => ({ ...choice })),
@@ -157,21 +165,38 @@ export default createPrompt(
157165
}
158166
});
159167

160-
const message = chalk.bold(config.message);
168+
const message = theme.style.message(config.message);
161169

162170
const page = usePagination<Item<Value>>({
163171
items,
164172
active,
165-
renderItem,
173+
renderItem({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
174+
if (Separator.isSeparator(item)) {
175+
return ` ${item.separator}`;
176+
}
177+
178+
const line = item.name || item.value;
179+
if (item.disabled) {
180+
const disabledLabel =
181+
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
182+
return theme.style.disabledChoice(`${line} ${disabledLabel}`);
183+
}
184+
185+
const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
186+
const color = isActive ? theme.style.highlight : (x: string) => x;
187+
const cursor = isActive ? theme.icon.cursor : ' ';
188+
return color(`${cursor}${checkbox} ${line}`);
189+
},
166190
pageSize,
167191
loop,
192+
theme,
168193
});
169194

170195
if (status === 'done') {
171196
const selection = items
172197
.filter(isChecked)
173198
.map((choice) => choice.name || choice.value);
174-
return `${prefix} ${message} ${chalk.cyan(selection.join(', '))}`;
199+
return `${prefix} ${message} ${theme.style.answer(selection.join(', '))}`;
175200
}
176201

177202
let helpTip = '';
@@ -180,18 +205,18 @@ export default createPrompt(
180205
helpTip = instructions;
181206
} else {
182207
const keys = [
183-
`${chalk.cyan.bold('<space>')} to select`,
184-
`${chalk.cyan.bold('<a>')} to toggle all`,
185-
`${chalk.cyan.bold('<i>')} to invert selection`,
186-
`and ${chalk.cyan.bold('<enter>')} to proceed`,
208+
`${theme.style.key('space')} to select`,
209+
`${theme.style.key('a')} to toggle all`,
210+
`${theme.style.key('i')} to invert selection`,
211+
`and ${theme.style.key('enter')} to proceed`,
187212
];
188213
helpTip = ` (Press ${keys.join(', ')})`;
189214
}
190215
}
191216

192217
let error = '';
193218
if (errorMsg) {
194-
error = chalk.red(`> ${errorMsg}`);
219+
error = theme.style.error(errorMsg);
195220
}
196221

197222
return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`;

packages/confirm/README.md

+25-5
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,31 @@ const answer = await confirm({ message: 'Continue?' });
2222

2323
## Options
2424

25-
| Property | Type | Required | Description |
26-
| ----------- | --------------------- | -------- | ------------------------------------------------------- |
27-
| message | `string` | yes | The question to ask |
28-
| default | `boolean` | no | Default answer (true or false) |
29-
| transformer | `(boolean) => string` | no | Transform the prompt printed message to a custom string |
25+
| Property | Type | Required | Description |
26+
| ----------- | ----------------------- | -------- | ------------------------------------------------------- |
27+
| message | `string` | yes | The question to ask |
28+
| default | `boolean` | no | Default answer (true or false) |
29+
| transformer | `(boolean) => string` | no | Transform the prompt printed message to a custom string |
30+
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
31+
32+
## Theming
33+
34+
You can theme a prompt by passing a `theme` object option. The theme object only need to includes the keys you wish to modify, we'll fallback on the defaults for the rest.
35+
36+
```ts
37+
type Theme = {
38+
prefix: string;
39+
spinner: {
40+
interval: number;
41+
frames: string[];
42+
};
43+
style: {
44+
answer: (text: string) => string;
45+
message: (text: string) => string;
46+
defaultAnswer: (text: string) => string;
47+
};
48+
};
49+
```
3050

3151
# License
3252

packages/confirm/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@
5555
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/master/packages/confirm/README.md",
5656
"dependencies": {
5757
"@inquirer/core": "^5.1.2",
58-
"@inquirer/type": "^1.1.6",
59-
"chalk": "^4.1.2"
58+
"@inquirer/type": "^1.1.6"
6059
},
6160
"devDependencies": {
6261
"@inquirer/testing": "^2.1.10"

packages/confirm/src/index.mts

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
1-
import chalk from 'chalk';
21
import {
32
createPrompt,
43
useState,
54
useKeypress,
65
isEnterKey,
76
usePrefix,
7+
makeTheme,
8+
type Theme,
89
} from '@inquirer/core';
9-
import type {} from '@inquirer/type';
10+
import type { PartialDeep } from '@inquirer/type';
1011

1112
type ConfirmConfig = {
1213
message: string;
1314
default?: boolean;
1415
transformer?: (value: boolean) => string;
16+
theme?: PartialDeep<Theme>;
1517
};
1618

1719
export default createPrompt<boolean, ConfirmConfig>((config, done) => {
1820
const { transformer = (answer) => (answer ? 'yes' : 'no') } = config;
1921
const [status, setStatus] = useState('pending');
2022
const [value, setValue] = useState('');
21-
const prefix = usePrefix();
23+
const theme = makeTheme(config.theme);
24+
const prefix = usePrefix({ theme });
2225

2326
useKeypress((key, rl) => {
2427
if (isEnterKey(key)) {
@@ -37,11 +40,13 @@ export default createPrompt<boolean, ConfirmConfig>((config, done) => {
3740
let formattedValue = value;
3841
let defaultValue = '';
3942
if (status === 'done') {
40-
formattedValue = chalk.cyan(value);
43+
formattedValue = theme.style.answer(value);
4144
} else {
42-
defaultValue = chalk.dim(config.default === false ? ' (y/N)' : ' (Y/n)');
45+
defaultValue = ` ${theme.style.defaultAnswer(
46+
config.default === false ? 'y/N' : 'Y/n',
47+
)}`;
4348
}
4449

45-
const message = chalk.bold(config.message);
50+
const message = theme.style.message(config.message);
4651
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
4752
});

packages/editor/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,27 @@ const answer = await editor({
2929
| validate | `string => boolean \| string \| Promise<string \| boolean>` | no | On submit, validate the content. When returning a string, it'll be used as the error message displayed to the user. Note: returning a rejected promise, we'll assume a code error happened and crash. |
3030
| postfix | `string` | no (default to `.txt`) | The postfix of the file being edited. Adding this will add color highlighting to the file content in most editors. |
3131
| waitForUseInput | `boolean` | no (default to `true`) | Open the editor automatically without waiting for the user to press enter. Note that this mean the user will not see the question! So make sure you have a default value that provide guidance if it's unclear what input is expected. |
32+
| theme | [See Theming](#Theming) | no | Customize look of the prompt. |
33+
34+
## Theming
35+
36+
You can theme a prompt by passing a `theme` object option. The theme object only need to includes the keys you wish to modify, we'll fallback on the defaults for the rest.
37+
38+
```ts
39+
type Theme = {
40+
prefix: string;
41+
spinner: {
42+
interval: number;
43+
frames: string[];
44+
};
45+
style: {
46+
message: (text: string) => string;
47+
error: (text: string) => string;
48+
help: (text: string) => string;
49+
key: (text: string) => string;
50+
};
51+
};
52+
```
3253

3354
# License
3455

packages/editor/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"dependencies": {
5757
"@inquirer/core": "^5.1.2",
5858
"@inquirer/type": "^1.1.6",
59-
"chalk": "^4.1.2",
6059
"external-editor": "^3.1.0"
6160
},
6261
"scripts": {

packages/editor/src/index.mts

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import chalk from 'chalk';
21
import { editAsync } from 'external-editor';
32
import {
43
createPrompt,
@@ -7,24 +6,32 @@ import {
76
useKeypress,
87
usePrefix,
98
isEnterKey,
9+
makeTheme,
1010
type InquirerReadline,
11+
type Theme,
1112
} from '@inquirer/core';
12-
import type {} from '@inquirer/type';
13+
import type { PartialDeep } from '@inquirer/type';
1314

1415
type EditorConfig = {
1516
message: string;
1617
default?: string;
1718
postfix?: string;
1819
waitForUseInput?: boolean;
1920
validate?: (value: string) => boolean | string | Promise<string | boolean>;
21+
theme?: PartialDeep<Theme>;
2022
};
2123

2224
export default createPrompt<string, EditorConfig>((config, done) => {
2325
const { waitForUseInput = true, validate = () => true } = config;
26+
const theme = makeTheme(config.theme);
27+
2428
const [status, setStatus] = useState<string>('pending');
2529
const [value, setValue] = useState<string>(config.default || '');
2630
const [errorMsg, setError] = useState<string | undefined>(undefined);
2731

32+
const isLoading = status === 'loading';
33+
const prefix = usePrefix({ isLoading, theme });
34+
2835
function startEditor(rl: InquirerReadline) {
2936
rl.pause();
3037
editAsync(
@@ -70,21 +77,18 @@ export default createPrompt<string, EditorConfig>((config, done) => {
7077
}
7178
});
7279

73-
const isLoading = status === 'loading';
74-
const prefix = usePrefix(isLoading);
75-
76-
const message = chalk.bold(config.message);
77-
78-
let helpTip;
80+
const message = theme.style.message(config.message);
81+
let helpTip = '';
7982
if (status === 'loading') {
80-
helpTip = chalk.dim('Received');
83+
helpTip = theme.style.help('Received');
8184
} else if (status === 'pending') {
82-
helpTip = chalk.dim('Press <enter> to launch your preferred editor.');
85+
const enterKey = theme.style.key('enter');
86+
helpTip = theme.style.help(`Press ${enterKey} to launch your preferred editor.`);
8387
}
8488

8589
let error = '';
8690
if (errorMsg) {
87-
error = chalk.red(`> ${errorMsg}`);
91+
error = theme.style.error(errorMsg);
8892
}
8993

9094
return [[prefix, message, helpTip].filter(Boolean).join(' '), error];

0 commit comments

Comments
 (0)