Skip to content

Commit

Permalink
Feat: Apply theming to all prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
SBoudrias committed Jan 27, 2024
1 parent 5709bfe commit 4eade75
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 100 deletions.
70 changes: 43 additions & 27 deletions packages/checkbox/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,34 @@ import {
usePrefix,
usePagination,
useMemo,
makeTheme,
isUpKey,
isDownKey,
isSpaceKey,
isNumberKey,
isEnterKey,
Separator,
type Theme,
} from '@inquirer/core';
import type {} from '@inquirer/type';
import chalk from 'chalk';
import figures from 'figures';
import ansiEscapes from 'ansi-escapes';

type CheckboxTheme = {
checkedIcon: string;
uncheckedIcon: string;
cursorIcon: string;
disabled: (text: string) => string;
};

const checkboxTheme: CheckboxTheme = {
checkedIcon: chalk.green(figures.circleFilled),
uncheckedIcon: figures.circle,
cursorIcon: figures.pointer,
disabled: (text: string) => chalk.dim(`- ${text}`),
};

type Choice<Value> = {
name?: string;
value: Value;
Expand All @@ -36,6 +52,7 @@ type Config<Value> = {
validate?: (
items: ReadonlyArray<Item<Value>>,
) => boolean | string | Promise<string | boolean>;
theme?: Partial<Theme<CheckboxTheme>>;
};

type Item<Value> = Separator | Choice<Value>;
Expand All @@ -58,35 +75,18 @@ function check(checked: boolean) {
};
}

function renderItem<Value>({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
if (Separator.isSeparator(item)) {
return ` ${item.separator}`;
}

const line = item.name || item.value;
if (item.disabled) {
const disabledLabel =
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
return chalk.dim(`- ${line} ${disabledLabel}`);
}

const checkbox = item.checked ? chalk.green(figures.circleFilled) : figures.circle;
const color = isActive ? chalk.cyan : (x: string) => x;
const prefix = isActive ? figures.pointer : ' ';
return color(`${prefix}${checkbox} ${line}`);
}

export default createPrompt(
<Value extends unknown>(config: Config<Value>, done: (value: Array<Value>) => void) => {
const {
prefix = usePrefix(),
instructions,
pageSize = 7,
loop = true,
choices,
required,
validate = () => true,
} = config;
const theme = makeTheme<CheckboxTheme>(checkboxTheme, config.theme);
const prefix = usePrefix({ theme });
const [status, setStatus] = useState('pending');
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
choices.map((choice) => ({ ...choice })),
Expand Down Expand Up @@ -157,12 +157,28 @@ export default createPrompt(
}
});

const message = chalk.bold(config.message);
const message = theme.style.message(config.message);

const page = usePagination<Item<Value>>({
items,
active,
renderItem,
renderItem({ item, isActive }: { item: Item<Value>; isActive: boolean }) {
if (Separator.isSeparator(item)) {
return ` ${item.separator}`;
}

const line = item.name || item.value;
if (item.disabled) {
const disabledLabel =
typeof item.disabled === 'string' ? item.disabled : '(disabled)';
return theme.disabled(`${line} ${disabledLabel}`);
}

const checkbox = item.checked ? theme.checkedIcon : theme.uncheckedIcon;
const color = isActive ? theme.style.highlight : (x: string) => x;
const cursor = isActive ? theme.cursorIcon : ' ';
return color(`${cursor}${checkbox} ${line}`);
},
pageSize,
loop,
});
Expand All @@ -171,7 +187,7 @@ export default createPrompt(
const selection = items
.filter(isChecked)
.map((choice) => choice.name || choice.value);
return `${prefix} ${message} ${chalk.cyan(selection.join(', '))}`;
return `${prefix} ${message} ${theme.style.answer(selection.join(', '))}`;
}

let helpTip = '';
Expand All @@ -180,18 +196,18 @@ export default createPrompt(
helpTip = instructions;
} else {
const keys = [
`${chalk.cyan.bold('<space>')} to select`,
`${chalk.cyan.bold('<a>')} to toggle all`,
`${chalk.cyan.bold('<i>')} to invert selection`,
`and ${chalk.cyan.bold('<enter>')} to proceed`,
`${theme.style.key('space')} to select`,
`${theme.style.key('a')} to toggle all`,
`${theme.style.key('i')} to invert selection`,
`and ${theme.style.key('enter')} to proceed`,
];
helpTip = ` (Press ${keys.join(', ')})`;
}
}

let error = '';
if (errorMsg) {
error = chalk.red(`> ${errorMsg}`);
error = theme.style.error(errorMsg);
}

return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`;
Expand Down
3 changes: 1 addition & 2 deletions packages/confirm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/master/packages/confirm/README.md",
"dependencies": {
"@inquirer/core": "^5.1.2",
"@inquirer/type": "^1.1.6",
"chalk": "^4.1.2"
"@inquirer/type": "^1.1.6"
},
"devDependencies": {
"@inquirer/testing": "^2.1.10"
Expand Down
15 changes: 10 additions & 5 deletions packages/confirm/src/index.mts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import chalk from 'chalk';
import {
createPrompt,
useState,
useKeypress,
isEnterKey,
usePrefix,
makeTheme,
type Theme,
} from '@inquirer/core';
import type {} from '@inquirer/type';

type ConfirmConfig = {
message: string;
default?: boolean;
transformer?: (value: boolean) => string;
theme?: Partial<Theme>;
};

export default createPrompt<boolean, ConfirmConfig>((config, done) => {
const { transformer = (answer) => (answer ? 'yes' : 'no') } = config;
const [status, setStatus] = useState('pending');
const [value, setValue] = useState('');
const prefix = usePrefix();
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });

useKeypress((key, rl) => {
if (isEnterKey(key)) {
Expand All @@ -37,11 +40,13 @@ export default createPrompt<boolean, ConfirmConfig>((config, done) => {
let formattedValue = value;
let defaultValue = '';
if (status === 'done') {
formattedValue = chalk.cyan(value);
formattedValue = theme.style.answer(value);
} else {
defaultValue = chalk.dim(config.default === false ? ' (y/N)' : ' (Y/n)');
defaultValue = ` ${theme.style.defaultAnswer(
config.default === false ? 'y/N' : 'Y/n',
)}`;
}

const message = chalk.bold(config.message);
const message = theme.style.message(config.message);
return `${prefix} ${message}${defaultValue} ${formattedValue}`;
});
1 change: 0 additions & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
"dependencies": {
"@inquirer/core": "^5.1.2",
"@inquirer/type": "^1.1.6",
"chalk": "^4.1.2",
"external-editor": "^3.1.0"
},
"scripts": {
Expand Down
24 changes: 14 additions & 10 deletions packages/editor/src/index.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import chalk from 'chalk';
import { editAsync } from 'external-editor';
import {
createPrompt,
Expand All @@ -7,7 +6,9 @@ import {
useKeypress,
usePrefix,
isEnterKey,
makeTheme,

Check warning on line 9 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L9

Added line #L9 was not covered by tests
type InquirerReadline,
type Theme,

Check warning on line 11 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L11

Added line #L11 was not covered by tests
} from '@inquirer/core';
import type {} from '@inquirer/type';

Expand All @@ -17,14 +18,20 @@ type EditorConfig = {
postfix?: string;
waitForUseInput?: boolean;
validate?: (value: string) => boolean | string | Promise<string | boolean>;
theme?: Partial<Theme>;
};

Check warning on line 22 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L21-L22

Added lines #L21 - L22 were not covered by tests

export default createPrompt<string, EditorConfig>((config, done) => {
const { waitForUseInput = true, validate = () => true } = config;
const theme = makeTheme(config.theme);

Check warning on line 27 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L26-L27

Added lines #L26 - L27 were not covered by tests
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>(config.default || '');
const [errorMsg, setError] = useState<string | undefined>(undefined);

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

Check warning on line 34 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L32-L34

Added lines #L32 - L34 were not covered by tests
function startEditor(rl: InquirerReadline) {
rl.pause();
editAsync(
Expand Down Expand Up @@ -70,21 +77,18 @@ export default createPrompt<string, EditorConfig>((config, done) => {
}
});

const isLoading = status === 'loading';
const prefix = usePrefix(isLoading);

const message = chalk.bold(config.message);

let helpTip;
const message = theme.style.message(config.message);
let helpTip = '';

Check warning on line 81 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L80-L81

Added lines #L80 - L81 were not covered by tests
if (status === 'loading') {
helpTip = chalk.dim('Received');
helpTip = theme.style.help('Received');

Check warning on line 83 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L83

Added line #L83 was not covered by tests
} else if (status === 'pending') {
helpTip = chalk.dim('Press <enter> to launch your preferred editor.');
const enterKey = theme.style.key('enter');
helpTip = theme.style.help(`Press ${enterKey} to launch your preferred editor.`);

Check warning on line 86 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L85-L86

Added lines #L85 - L86 were not covered by tests
}

let error = '';
if (errorMsg) {
error = chalk.red(`> ${errorMsg}`);
error = theme.style.error(errorMsg);

Check warning on line 91 in packages/editor/src/index.mts

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L91

Added line #L91 was not covered by tests
}

return [[prefix, message, helpTip].filter(Boolean).join(' '), error];
Expand Down
16 changes: 10 additions & 6 deletions packages/expand/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
useKeypress,
usePrefix,
isEnterKey,
makeTheme,
type Theme,
} from '@inquirer/core';
import type {} from '@inquirer/type';
import chalk from 'chalk';
Expand All @@ -18,6 +20,7 @@ type ExpandConfig = {
choices: ReadonlyArray<ExpandChoice>;
default?: string;
expanded?: boolean;
theme?: Partial<Theme>;
};

const helpChoice = {
Expand Down Expand Up @@ -46,7 +49,8 @@ export default createPrompt<string, ExpandConfig>((config, done) => {
const [value, setValue] = useState<string>('');
const [expanded, setExpanded] = useState<boolean>(defaultExpandState);
const [errorMsg, setError] = useState<string | undefined>(undefined);
const prefix = usePrefix();
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });

useKeypress((event, rl) => {
if (isEnterKey(event)) {
Expand All @@ -72,11 +76,11 @@ export default createPrompt<string, ExpandConfig>((config, done) => {
}
});

const message = chalk.bold(config.message);
const message = theme.style.message(config.message);

if (status === 'done') {
// TODO: `value` should be the display name instead of the raw value.

Check warning on line 82 in packages/expand/src/index.mts

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: `value` should be the display name...'

Check warning on line 82 in packages/expand/src/index.mts

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: `value` should be the display name...'
return `${prefix} ${message} ${chalk.cyan(value)}`;
return `${prefix} ${message} ${theme.style.answer(value)}`;
}

const allChoices = expanded ? choices : [...choices, helpChoice];
Expand All @@ -92,7 +96,7 @@ export default createPrompt<string, ExpandConfig>((config, done) => {
return choice.key;
})
.join('');
shortChoices = chalk.dim(` (${shortChoices})`);
shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`;

// Expanded display style
if (expanded) {
Expand All @@ -101,7 +105,7 @@ export default createPrompt<string, ExpandConfig>((config, done) => {
.map((choice) => {
const line = ` ${choice.key}) ${getChoiceKey(choice, 'name')}`;
if (choice.key === value.toLowerCase()) {
return chalk.cyan(line);
return theme.style.highlight(line);
}

return line;
Expand All @@ -117,7 +121,7 @@ export default createPrompt<string, ExpandConfig>((config, done) => {

let error = '';
if (errorMsg) {
error = chalk.red(`> ${errorMsg}`);
error = theme.style.error(errorMsg);
}

return [
Expand Down
3 changes: 1 addition & 2 deletions packages/input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@
"homepage": "https://github.com/SBoudrias/Inquirer.js/blob/master/packages/input/README.md",
"dependencies": {
"@inquirer/core": "^5.1.2",
"@inquirer/type": "^1.1.6",
"chalk": "^4.1.2"
"@inquirer/type": "^1.1.6"
},
"devDependencies": {
"@inquirer/testing": "^2.1.10"
Expand Down
Loading

0 comments on commit 4eade75

Please sign in to comment.