diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index 5b70675d59..0c60814d6a 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -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 = { name?: string; value: Value; @@ -36,6 +52,7 @@ type Config = { validate?: ( items: ReadonlyArray>, ) => boolean | string | Promise; + theme?: Partial>; }; type Item = Separator | Choice; @@ -58,28 +75,9 @@ function check(checked: boolean) { }; } -function renderItem({ item, isActive }: { item: Item; 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( (config: Config, done: (value: Array) => void) => { const { - prefix = usePrefix(), instructions, pageSize = 7, loop = true, @@ -87,6 +85,8 @@ export default createPrompt( required, validate = () => true, } = config; + const theme = makeTheme(checkboxTheme, config.theme); + const prefix = usePrefix({ theme }); const [status, setStatus] = useState('pending'); const [items, setItems] = useState>>( choices.map((choice) => ({ ...choice })), @@ -157,12 +157,28 @@ export default createPrompt( } }); - const message = chalk.bold(config.message); + const message = theme.style.message(config.message); const page = usePagination>({ items, active, - renderItem, + renderItem({ item, isActive }: { item: Item; 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, }); @@ -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 = ''; @@ -180,10 +196,10 @@ export default createPrompt( helpTip = instructions; } else { const keys = [ - `${chalk.cyan.bold('')} to select`, - `${chalk.cyan.bold('')} to toggle all`, - `${chalk.cyan.bold('')} to invert selection`, - `and ${chalk.cyan.bold('')} 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(', ')})`; } @@ -191,7 +207,7 @@ export default createPrompt( let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`; diff --git a/packages/confirm/package.json b/packages/confirm/package.json index 8a1fc87dd8..f38c69f904 100644 --- a/packages/confirm/package.json +++ b/packages/confirm/package.json @@ -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" diff --git a/packages/confirm/src/index.mts b/packages/confirm/src/index.mts index 5c8faa575f..6bcb157410 100644 --- a/packages/confirm/src/index.mts +++ b/packages/confirm/src/index.mts @@ -1,10 +1,11 @@ -import chalk from 'chalk'; import { createPrompt, useState, useKeypress, isEnterKey, usePrefix, + makeTheme, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; @@ -12,13 +13,15 @@ type ConfirmConfig = { message: string; default?: boolean; transformer?: (value: boolean) => string; + theme?: Partial; }; export default createPrompt((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)) { @@ -37,11 +40,13 @@ export default createPrompt((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}`; }); diff --git a/packages/editor/package.json b/packages/editor/package.json index 998612ecbf..3a4b308f41 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -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": { diff --git a/packages/editor/src/index.mts b/packages/editor/src/index.mts index 7756094a6e..7286ca7007 100644 --- a/packages/editor/src/index.mts +++ b/packages/editor/src/index.mts @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import { editAsync } from 'external-editor'; import { createPrompt, @@ -7,7 +6,9 @@ import { useKeypress, usePrefix, isEnterKey, + makeTheme, type InquirerReadline, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; @@ -17,14 +18,20 @@ type EditorConfig = { postfix?: string; waitForUseInput?: boolean; validate?: (value: string) => boolean | string | Promise; + theme?: Partial; }; export default createPrompt((config, done) => { const { waitForUseInput = true, validate = () => true } = config; + const theme = makeTheme(config.theme); + const [status, setStatus] = useState('pending'); const [value, setValue] = useState(config.default || ''); const [errorMsg, setError] = useState(undefined); + const isLoading = status === 'loading'; + const prefix = usePrefix({ isLoading, theme }); + function startEditor(rl: InquirerReadline) { rl.pause(); editAsync( @@ -70,21 +77,18 @@ export default createPrompt((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 = ''; if (status === 'loading') { - helpTip = chalk.dim('Received'); + helpTip = theme.style.help('Received'); } else if (status === 'pending') { - helpTip = chalk.dim('Press to launch your preferred editor.'); + const enterKey = theme.style.key('enter'); + helpTip = theme.style.help(`Press ${enterKey} to launch your preferred editor.`); } let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return [[prefix, message, helpTip].filter(Boolean).join(' '), error]; diff --git a/packages/expand/src/index.mts b/packages/expand/src/index.mts index 50f5f6c82f..af148c7a12 100644 --- a/packages/expand/src/index.mts +++ b/packages/expand/src/index.mts @@ -4,6 +4,8 @@ import { useKeypress, usePrefix, isEnterKey, + makeTheme, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; import chalk from 'chalk'; @@ -18,6 +20,7 @@ type ExpandConfig = { choices: ReadonlyArray; default?: string; expanded?: boolean; + theme?: Partial; }; const helpChoice = { @@ -46,7 +49,8 @@ export default createPrompt((config, done) => { const [value, setValue] = useState(''); const [expanded, setExpanded] = useState(defaultExpandState); const [errorMsg, setError] = useState(undefined); - const prefix = usePrefix(); + const theme = makeTheme(config.theme); + const prefix = usePrefix({ theme }); useKeypress((event, rl) => { if (isEnterKey(event)) { @@ -72,11 +76,11 @@ export default createPrompt((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. - return `${prefix} ${message} ${chalk.cyan(value)}`; + return `${prefix} ${message} ${theme.style.answer(value)}`; } const allChoices = expanded ? choices : [...choices, helpChoice]; @@ -92,7 +96,7 @@ export default createPrompt((config, done) => { return choice.key; }) .join(''); - shortChoices = chalk.dim(` (${shortChoices})`); + shortChoices = ` ${theme.style.defaultAnswer(shortChoices)}`; // Expanded display style if (expanded) { @@ -101,7 +105,7 @@ export default createPrompt((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; @@ -117,7 +121,7 @@ export default createPrompt((config, done) => { let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return [ diff --git a/packages/input/package.json b/packages/input/package.json index 863986f98c..089460d4f3 100644 --- a/packages/input/package.json +++ b/packages/input/package.json @@ -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" diff --git a/packages/input/src/index.mts b/packages/input/src/index.mts index 808aeb980e..2dcbe2f67e 100644 --- a/packages/input/src/index.mts +++ b/packages/input/src/index.mts @@ -5,19 +5,22 @@ import { usePrefix, isEnterKey, isBackspaceKey, + makeTheme, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; -import chalk from 'chalk'; type InputConfig = { message: string; default?: string; transformer?: (value: string, { isFinal }: { isFinal: boolean }) => string; validate?: (value: string) => boolean | string | Promise; + theme?: Partial; }; export default createPrompt((config, done) => { const { validate = () => true } = config; + const theme = makeTheme(config.theme); const [status, setStatus] = useState('pending'); const [defaultValue = '', setDefaultValue] = useState( config.default, @@ -26,7 +29,7 @@ export default createPrompt((config, done) => { const [value, setValue] = useState(''); const isLoading = status === 'loading'; - const prefix = usePrefix(isLoading); + const prefix = usePrefix({ isLoading, theme }); useKeypress(async (key, rl) => { // Ignore keypress while our prompt is doing other processing. @@ -62,22 +65,22 @@ export default createPrompt((config, done) => { } }); - const message = chalk.bold(config.message); + const message = theme.style.message(config.message); let formattedValue = value; if (typeof config.transformer === 'function') { formattedValue = config.transformer(value, { isFinal: status === 'done' }); } else if (status === 'done') { - formattedValue = chalk.cyan(value); + formattedValue = theme.style.answer(value); } let defaultStr; if (defaultValue && status !== 'done' && !value) { - defaultStr = chalk.dim(`(${defaultValue})`); + defaultStr = theme.style.defaultAnswer(defaultValue); } let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return [[prefix, message, defaultStr, formattedValue].filter(Boolean).join(' '), error]; diff --git a/packages/password/package.json b/packages/password/package.json index 26c440a014..446ac8c78f 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -59,8 +59,7 @@ "dependencies": { "@inquirer/core": "^5.1.2", "@inquirer/type": "^1.1.6", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2" + "ansi-escapes": "^4.3.2" }, "devDependencies": { "@inquirer/testing": "^2.1.10" diff --git a/packages/password/src/index.mts b/packages/password/src/index.mts index 03d768fdeb..3506d74268 100644 --- a/packages/password/src/index.mts +++ b/packages/password/src/index.mts @@ -4,24 +4,28 @@ import { useKeypress, usePrefix, isEnterKey, + makeTheme, + type Theme, } from '@inquirer/core'; -import chalk from 'chalk'; import ansiEscapes from 'ansi-escapes'; type PasswordConfig = { message: string; mask?: boolean | string; validate?: (value: string) => boolean | string | Promise; + theme?: Partial; }; export default createPrompt((config, done) => { const { validate = () => true } = config; + const theme = makeTheme(config.theme); + const [status, setStatus] = useState('pending'); const [errorMsg, setError] = useState(undefined); const [value, setValue] = useState(''); const isLoading = status === 'loading'; - const prefix = usePrefix(isLoading); + const prefix = usePrefix({ isLoading, theme }); useKeypress(async (key, rl) => { // Ignore keypress while our prompt is doing other processing. @@ -50,7 +54,7 @@ export default createPrompt((config, done) => { } }); - const message = chalk.bold(config.message); + const message = theme.style.message(config.message); let formattedValue = ''; let helpTip; @@ -58,16 +62,16 @@ export default createPrompt((config, done) => { const maskChar = typeof config.mask === 'string' ? config.mask : '*'; formattedValue = maskChar.repeat(value.length); } else if (status !== 'done') { - helpTip = `${chalk.dim('[input is masked]')}${ansiEscapes.cursorHide}`; + helpTip = `${theme.style.help('[input is masked]')}${ansiEscapes.cursorHide}`; } if (status === 'done') { - formattedValue = chalk.cyan(formattedValue); + formattedValue = theme.style.answer(formattedValue); } let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return [[prefix, message, formattedValue, helpTip].filter(Boolean).join(' '), error]; diff --git a/packages/rawlist/src/index.mts b/packages/rawlist/src/index.mts index 606e130c99..d61a6a990a 100644 --- a/packages/rawlist/src/index.mts +++ b/packages/rawlist/src/index.mts @@ -5,6 +5,8 @@ import { usePrefix, isEnterKey, Separator, + makeTheme, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; import chalk from 'chalk'; @@ -20,6 +22,7 @@ type Choice = { type RawlistConfig = { message: string; choices: ReadonlyArray | Separator>; + theme?: Partial; }; function isSelectableChoice( @@ -34,7 +37,8 @@ export default createPrompt( const [status, setStatus] = useState('pending'); const [value, setValue] = useState(''); const [errorMsg, setError] = useState(undefined); - const prefix = usePrefix(); + const theme = makeTheme(config.theme); + const prefix = usePrefix({ theme }); useKeypress((key, rl) => { if (isEnterKey(key)) { @@ -64,10 +68,10 @@ export default createPrompt( } }); - const message = chalk.bold(config.message); + const message = theme.style.message(config.message); if (status === 'done') { - return `${prefix} ${message} ${chalk.cyan(value)}`; + return `${prefix} ${message} ${theme.style.answer(value)}`; } let index = 0; @@ -81,7 +85,7 @@ export default createPrompt( const line = ` ${choice.key || index}) ${choice.name || choice.value}`; if (choice.key === value.toLowerCase() || String(index) === value) { - return chalk.cyan(line); + return theme.style.highlight(line); } return line; @@ -90,7 +94,7 @@ export default createPrompt( let error = ''; if (errorMsg) { - error = chalk.red(`> ${errorMsg}`); + error = theme.style.error(errorMsg); } return [ diff --git a/packages/select/src/index.mts b/packages/select/src/index.mts index 634a5b7190..3ee2b4db1f 100644 --- a/packages/select/src/index.mts +++ b/packages/select/src/index.mts @@ -11,12 +11,24 @@ import { isDownKey, isNumberKey, Separator, + makeTheme, + type Theme, } from '@inquirer/core'; import type {} from '@inquirer/type'; import chalk from 'chalk'; import figures from 'figures'; import ansiEscapes from 'ansi-escapes'; +type SelectTheme = { + cursorIcon: string; + disabled: (text: string) => string; +}; + +const selectTheme: SelectTheme = { + cursorIcon: figures.pointer, + disabled: (text: string) => chalk.dim(`- ${text}`), +}; + type Choice = { value: Value; name?: string; @@ -31,6 +43,7 @@ type SelectConfig = { pageSize?: number; loop?: boolean; default?: Value; + theme?: Partial; }; type Item = Separator | Choice; @@ -39,23 +52,6 @@ function isSelectable(item: Item): item is Choice { return !Separator.isSeparator(item) && !item.disabled; } -function renderItem({ item, isActive }: { item: Item; 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 color = isActive ? chalk.cyan : (x: string) => x; - const prefix = isActive ? figures.pointer : ` `; - return color(`${prefix} ${line}`); -} - export default createPrompt( ( config: SelectConfig, @@ -63,7 +59,8 @@ export default createPrompt( ): string => { const { choices: items, loop = true, pageSize = 7 } = config; const firstRender = useRef(true); - const prefix = usePrefix(); + const theme = makeTheme(selectTheme, config.theme); + const prefix = usePrefix({ theme }); const [status, setStatus] = useState('pending'); const bounds = useMemo(() => { @@ -117,26 +114,43 @@ export default createPrompt( } }); - const message = chalk.bold(config.message); + const message = theme.style.message(config.message); let helpTip; if (firstRender.current && items.length <= pageSize) { firstRender.current = false; - helpTip = chalk.dim('(Use arrow keys)'); + helpTip = theme.style.help('(Use arrow keys)'); } const page = usePagination>({ items, active, - renderItem, + renderItem({ item, isActive }: { item: Item; 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 color = isActive ? theme.style.highlight : (x: string) => x; + const cursor = isActive ? theme.cursorIcon : ` `; + return color(`${cursor} ${line}`); + }, pageSize, loop, }); if (status === 'done') { - return `${prefix} ${message} ${chalk.cyan( - selectedChoice.name || selectedChoice.value, - )}`; + const answer = + selectedChoice.name || + // TODO: Could we enforce that at the type level? Name should be defined for non-string values. + String(selectedChoice.value); + return `${prefix} ${message} ${theme.style.answer(answer)}`; } const choiceDescription = selectedChoice.description diff --git a/yarn.lock b/yarn.lock index 8270e611e1..0588d38bc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -373,7 +373,6 @@ __metadata: "@inquirer/core": ^5.1.2 "@inquirer/testing": ^2.1.10 "@inquirer/type": ^1.1.6 - chalk: ^4.1.2 languageName: unknown linkType: soft @@ -416,7 +415,6 @@ __metadata: dependencies: "@inquirer/core": ^5.1.2 "@inquirer/type": ^1.1.6 - chalk: ^4.1.2 external-editor: ^3.1.0 languageName: unknown linkType: soft @@ -448,7 +446,6 @@ __metadata: "@inquirer/core": ^5.1.2 "@inquirer/testing": ^2.1.10 "@inquirer/type": ^1.1.6 - chalk: ^4.1.2 languageName: unknown linkType: soft @@ -460,7 +457,6 @@ __metadata: "@inquirer/testing": ^2.1.10 "@inquirer/type": ^1.1.6 ansi-escapes: ^4.3.2 - chalk: ^4.1.2 languageName: unknown linkType: soft