Skip to content

Commit

Permalink
Feat: Add basic 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 44b1cda commit 04ebde8
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 110 deletions.
73 changes: 45 additions & 28 deletions packages/checkbox/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
usePrefix,
usePagination,
useMemo,
makeTheme,
isUpKey,
isDownKey,
isSpaceKey,
Expand All @@ -18,6 +19,20 @@ 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 @@ -37,7 +52,11 @@ type Config<Value> = {
validate?: (
items: ReadonlyArray<Item<Value>>,
) => boolean | string | Promise<string | boolean>;
theme?: Partial<Theme>;
theme?: Partial<
Theme<CheckboxTheme> & {
style: Partial<Theme<CheckboxTheme>['style']>;
}
>;
};

type Item<Value> = Separator | Choice<Value>;
Expand All @@ -60,24 +79,6 @@ 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 {
Expand All @@ -87,8 +88,8 @@ export default createPrompt(
choices,
required,
validate = () => true,
theme,
} = config;
const theme = makeTheme<CheckboxTheme>(checkboxTheme, config.theme);
const prefix = usePrefix({ theme });
const [status, setStatus] = useState('pending');
const [items, setItems] = useState<ReadonlyArray<Item<Value>>>(
Expand Down Expand Up @@ -160,12 +161,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 @@ -174,7 +191,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 @@ -183,18 +200,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
13 changes: 8 additions & 5 deletions packages/confirm/src/index.mts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import chalk from 'chalk';
import {
createPrompt,
useState,
useKeypress,
isEnterKey,
usePrefix,
makeTheme,
type Theme,
} from '@inquirer/core';
import type {} from '@inquirer/type';
Expand All @@ -17,9 +17,10 @@ type ConfirmConfig = {
};

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

useKeypress((key, rl) => {
Expand All @@ -39,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}`;
});
13 changes: 6 additions & 7 deletions packages/core/src/lib/make-theme.mts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { Prettify } from '@inquirer/type';
import { defaultTheme, type Theme } from './theme.mjs';

export function makeTheme<SpecificTheme extends {}>(
defaultSpecificTheme: SpecificTheme,
theme: Partial<SpecificTheme> = {},
): Prettify<SpecificTheme> {
return {
...defaultSpecificTheme,
...theme,
};
...themes: ReadonlyArray<undefined | Partial<Theme<SpecificTheme>>>
): Prettify<Theme<SpecificTheme>> {
return Object.assign({}, defaultTheme, ...themes, {
style: Object.assign({}, defaultTheme.style, ...themes.map((theme) => theme?.style)),
});
}
31 changes: 30 additions & 1 deletion packages/core/src/lib/theme.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
export type Theme = {
import chalk from 'chalk';
import spinners from 'cli-spinners';
import type { Prettify } from '@inquirer/type';

export type DefaultTheme = {
prefix: string;
spinner: {
interval: number;
frames: string[];
};
style: {
answer: (text: string) => string;
message: (text: string) => string;
error: (text: string) => string;
defaultAnswer: (text: string) => string;
help: (text: string) => string;
highlight: (text: string) => string;
key: (text: string) => string;
};
};

export type Theme<Extension extends {} = {}> = Prettify<Extension & DefaultTheme>;

export const defaultTheme: DefaultTheme = {
prefix: chalk.green('?'),
spinner: spinners.dots,
style: {
answer: chalk.cyan,
message: chalk.bold,
error: (text) => chalk.red(`> ${text}`),
defaultAnswer: (text) => chalk.dim(`(${text})`),
help: (text) => chalk.dim(text),
highlight: chalk.cyan,
key: (text: string) => chalk.cyan.bold(`<${text}>`),
},
};
10 changes: 2 additions & 8 deletions packages/core/src/lib/use-prefix.mts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import chalk from 'chalk';
import spinners from 'cli-spinners';
import { useState } from './use-state.mjs';
import { useEffect } from './use-effect.mjs';
import { makeTheme } from './make-theme.mjs';
import type { Theme } from './theme.mjs';

const prefixTheme = {
prefix: chalk.green('?'),
spinner: spinners.dots,
} satisfies Partial<Theme>;
import { type Theme } from './theme.mjs';

export function usePrefix({
isLoading = false,
Expand All @@ -18,7 +12,7 @@ export function usePrefix({
theme?: Partial<Theme>;
}): string {
const [tick, setTick] = useState(0);
const { prefix, spinner } = makeTheme(prefixTheme, theme);
const { prefix, spinner } = makeTheme(theme);

useEffect((): void | (() => unknown) => {
if (isLoading) {
Expand Down
25 changes: 15 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,6 +6,7 @@ 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';
Expand All @@ -22,11 +22,15 @@ type EditorConfig = {
};

export default createPrompt<string, EditorConfig>((config, done) => {
const { waitForUseInput = true, validate = () => true, theme } = config;
const { waitForUseInput = true, validate = () => true } = config;
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>(config.default || '');
const [errorMsg, setError] = useState<string | undefined>(undefined);

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

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

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L30-L33

Added lines #L30 - L33 were not covered by tests
function startEditor(rl: InquirerReadline) {
rl.pause();
editAsync(
Expand Down Expand Up @@ -72,20 +76,21 @@ export default createPrompt<string, EditorConfig>((config, done) => {
}
});

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L82

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

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

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L84-L87

Added lines #L84 - L87 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L92

Added line #L92 was not covered by tests
}

return [`${prefix} ${message}`, error];
return [`${prefix} ${message} ${helpTip}`, error];

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

View check run for this annotation

Codecov / codecov/patch

packages/editor/src/index.mts#L95

Added line #L95 was not covered by tests
});
13 changes: 7 additions & 6 deletions packages/expand/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useKeypress,
usePrefix,
isEnterKey,
makeTheme,
type Theme,
} from '@inquirer/core';
import type {} from '@inquirer/type';
Expand Down Expand Up @@ -43,12 +44,12 @@ export default createPrompt<string, ExpandConfig>((config, done) => {
choices,
default: defaultKey = 'h',
expanded: defaultExpandState = false,
theme,
} = config;
const [status, setStatus] = useState<string>('pending');
const [value, setValue] = useState<string>('');
const [expanded, setExpanded] = useState<boolean>(defaultExpandState);
const [errorMsg, setError] = useState<string | undefined>(undefined);
const theme = makeTheme(config.theme);
const prefix = usePrefix({ theme });

useKeypress((event, rl) => {
Expand All @@ -75,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 @@ -95,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 @@ -104,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 @@ -120,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
Loading

0 comments on commit 04ebde8

Please sign in to comment.