From 196f954efbe46021404efb94809aa167be82ece1 Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Fri, 12 Apr 2024 15:01:12 -0400 Subject: [PATCH] Feat: Expose customization options for Select/Checkbox prompts help tips (#1381) * Chore: Add type tests to @inquirer/prompts * Feat: Expose customization options for Select/Checkbox prompts help tips Fix #963 Fix #1217 --- packages/checkbox/README.md | 7 + packages/checkbox/checkbox.test.mts | 317 +++++++++++++----- packages/checkbox/src/index.mts | 32 +- packages/core/README.md | 1 - .../src/lib/pagination/use-pagination.mts | 11 +- packages/prompts/package.json | 3 + packages/prompts/prompts.test.mts | 11 + packages/select/README.md | 7 + packages/select/select.test.mts | 139 +++++++- packages/select/src/index.mts | 20 +- packages/type/src/index.mts | 22 ++ yarn.lock | 3 +- 12 files changed, 459 insertions(+), 114 deletions(-) diff --git a/packages/checkbox/README.md b/packages/checkbox/README.md index b188c4942..88587a6f5 100644 --- a/packages/checkbox/README.md +++ b/packages/checkbox/README.md @@ -77,9 +77,16 @@ type Theme = { unchecked: string; cursor: string; }; + helpMode: 'always' | 'never' | 'auto'; }; ``` +### `theme.helpMode` + +- `auto` (default): Hide the help tips after an interaction occurs. The scroll tip will hide after any interactions, the selection tip will hide as soon as a first selection is done. +- `always`: The help tips will always show and never hide. +- `never`: The help tips will never show. + # License Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))
diff --git a/packages/checkbox/checkbox.test.mts b/packages/checkbox/checkbox.test.mts index 1e2fd02cb..2d4bd88ed 100644 --- a/packages/checkbox/checkbox.test.mts +++ b/packages/checkbox/checkbox.test.mts @@ -50,8 +50,7 @@ describe('checkbox prompt', () => { ◯ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('enter'); @@ -89,8 +88,7 @@ describe('checkbox prompt', () => { ◯ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('enter'); @@ -128,8 +126,7 @@ describe('checkbox prompt', () => { ◯ 3 ◯ 4 ◯ 5 - ◯ 6 - (Use arrow keys to reveal more choices)" + ◯ 6" `); events.keypress('enter'); @@ -168,8 +165,7 @@ describe('checkbox prompt', () => { ◯ 9 ◯ 10 ◯ 11 - ❯◉ 12 - (Use arrow keys to reveal more choices)" + ❯◉ 12" `); events.keypress('enter'); @@ -208,8 +204,7 @@ describe('checkbox prompt', () => { ◯ 10 ◯ 11 ❯◉ 12 - ────────────── - (Use arrow keys to reveal more choices)" + ──────────────" `); events.keypress('enter'); @@ -233,8 +228,7 @@ describe('checkbox prompt', () => { ❯◉ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('enter'); @@ -310,8 +304,7 @@ describe('checkbox prompt', () => { expect(getScreen()).toMatchInlineSnapshot(` "? Select a number ❯◉ 11 - ◉ 12 - (Use arrow keys to reveal more choices)" + ◉ 12" `); events.keypress('enter'); @@ -462,8 +455,7 @@ describe('checkbox prompt', () => { ❯◉ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('a'); @@ -476,8 +468,7 @@ describe('checkbox prompt', () => { ❯◉ 4 ◉ 5 ◉ 6 - ◉ 7 - (Use arrow keys to reveal more choices)" + ◉ 7" `); events.keypress('a'); @@ -490,8 +481,7 @@ describe('checkbox prompt', () => { ❯◯ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('a'); @@ -515,8 +505,7 @@ describe('checkbox prompt', () => { ❯◉ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('a'); @@ -544,8 +533,7 @@ describe('checkbox prompt', () => { ❯◉ 8 ◯ 9 ◯ 10 - ◯ 11 - (Use arrow keys to reveal more choices)" + ◯ 11" `); events.keypress('i'); @@ -568,8 +556,7 @@ describe('checkbox prompt', () => { ◯ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('enter'); @@ -634,7 +621,6 @@ describe('checkbox prompt', () => { ◯ 5 ◯ 6 ◯ 7 - (Use arrow keys to reveal more choices) > At least one choice must be selected" `); @@ -647,8 +633,7 @@ describe('checkbox prompt', () => { ◯ 4 ◯ 5 ◯ 6 - ◯ 7 - (Use arrow keys to reveal more choices)" + ◯ 7" `); events.keypress('enter'); @@ -679,7 +664,6 @@ describe('checkbox prompt', () => { ◯ 5 ◯ 6 ◯ 7 - (Use arrow keys to reveal more choices) > Please select only one choice" `); @@ -688,63 +672,238 @@ describe('checkbox prompt', () => { await expect(answer).resolves.toEqual([1]); }); - it('renderSelectedChoices', async () => { - const { answer, events, getScreen } = await render(checkbox, { - message: 'Select your favourite number.', - choices: numberedChoices, - theme: { - style: { - renderSelectedChoices(selected: { value: number }[]) { - if (selected.length > 1) { - return `You have selected ${selected[0].value} and ${selected.length - 1} more.`; - } - return `You have selected ${selected - .slice(0, 1) - .map((c) => c.value) - .join(', ')}.`; + describe('theme: style.renderSelectedChoices', () => { + it('renderSelectedChoices', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select your favourite number.', + choices: numberedChoices, + theme: { + style: { + renderSelectedChoices(selected: { value: number }[]) { + if (selected.length > 1) { + return `You have selected ${selected[0].value} and ${selected.length - 1} more.`; + } + return `You have selected ${selected + .slice(0, 1) + .map((c) => c.value) + .join(', ')}.`; + }, }, }, - }, + }); + + events.keypress('space'); + events.keypress('down'); + events.keypress('space'); + events.keypress('down'); + events.keypress('space'); + events.keypress('enter'); + + await answer; + expect(getScreen()).toMatchInlineSnapshot( + '"? Select your favourite number. You have selected 1 and 2 more."', + ); }); - events.keypress('space'); - events.keypress('down'); - events.keypress('space'); - events.keypress('down'); - events.keypress('space'); - events.keypress('enter'); - - await answer; - expect(getScreen()).toMatchInlineSnapshot( - '"? Select your favourite number. You have selected 1 and 2 more."', - ); - }); - - it('renderSelectedChoices - using allChoices parameter', async () => { - const { answer, events, getScreen } = await render(checkbox, { - message: 'Select your favourite number.', - choices: numberedChoices, - theme: { - style: { - renderSelectedChoices( - selected: { value: number }[], - all: ({ value: number } | Separator)[], - ) { - return `You have selected ${selected.length} out of ${all.length} options.`; + it('using allChoices parameter', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select your favourite number.', + choices: numberedChoices, + theme: { + style: { + renderSelectedChoices( + selected: { value: number }[], + all: ({ value: number } | Separator)[], + ) { + return `You have selected ${selected.length} out of ${all.length} options.`; + }, }, }, - }, + }); + + events.keypress('space'); + events.keypress('down'); + events.keypress('down'); + events.keypress('space'); + events.keypress('enter'); + + await answer; + expect(getScreen()).toMatchInlineSnapshot( + '"? Select your favourite number. You have selected 2 out of 12 options."', + ); }); + }); - events.keypress('space'); - events.keypress('down'); - events.keypress('down'); - events.keypress('space'); - events.keypress('enter'); + describe('theme: helpMode', () => { + const scrollTip = '(Use arrow keys to reveal more choices)'; + const selectTip = 'Press to select'; + + it('helpMode: auto', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'auto' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ❯◯ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + expect(getScreen()).toContain(selectTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ◯ 1 + ❯◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7" + `); + expect(getScreen()).not.toContain(scrollTip); + expect(getScreen()).toContain(selectTip); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ◯ 1 + ❯◉ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7" + `); + expect(getScreen()).not.toContain(scrollTip); + expect(getScreen()).not.toContain(selectTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual([2]); + expect(getScreen()).toMatchInlineSnapshot(`"? Select a number 2"`); + }); - await answer; - expect(getScreen()).toMatchInlineSnapshot( - '"? Select your favourite number. You have selected 2 out of 12 options."', - ); + it('helpMode: always', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'always' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ❯◯ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + expect(getScreen()).toContain(selectTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ◯ 1 + ❯◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + expect(getScreen()).toContain(selectTip); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number (Press to select, to toggle all, to invert + selection, and to proceed) + ◯ 1 + ❯◉ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + expect(getScreen()).toContain(selectTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual([2]); + expect(getScreen()).toMatchInlineSnapshot(`"? Select a number 2"`); + }); + + it('helpMode: never', async () => { + const { answer, events, getScreen } = await render(checkbox, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'never' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯◯ 1 + ◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7" + `); + expect(getScreen()).not.toContain(scrollTip); + expect(getScreen()).not.toContain(selectTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ◯ 1 + ❯◯ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7" + `); + expect(getScreen()).not.toContain(scrollTip); + expect(getScreen()).not.toContain(selectTip); + + events.keypress('space'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ◯ 1 + ❯◉ 2 + ◯ 3 + ◯ 4 + ◯ 5 + ◯ 6 + ◯ 7" + `); + expect(getScreen()).not.toContain(scrollTip); + expect(getScreen()).not.toContain(selectTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual([2]); + expect(getScreen()).toMatchInlineSnapshot(`"? Select a number 2"`); + }); }); }); diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index 3fae3ffd3..969e89e33 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -4,6 +4,7 @@ import { useKeypress, usePrefix, usePagination, + useRef, useMemo, makeTheme, isUpKey, @@ -33,6 +34,7 @@ type CheckboxTheme = { allChoices: ReadonlyArray | Separator>, ) => string; }; + helpMode: 'always' | 'never' | 'auto'; }; const checkboxTheme: CheckboxTheme = { @@ -46,6 +48,7 @@ const checkboxTheme: CheckboxTheme = { renderSelectedChoices: (selectedChoices) => selectedChoices.map((choice) => choice.name || choice.value).join(', '), }, + helpMode: 'auto', }; type Choice = { @@ -102,6 +105,7 @@ export default createPrompt( } = config; const theme = makeTheme(checkboxTheme, config.theme); const prefix = usePrefix({ theme }); + const firstRender = useRef(true); const [status, setStatus] = useState('pending'); const [items, setItems] = useState>>( choices.map((choice) => ({ ...choice })), @@ -195,7 +199,6 @@ export default createPrompt( }, pageSize, loop, - theme, }); if (status === 'done') { @@ -207,10 +210,16 @@ export default createPrompt( return `${prefix} ${message} ${answer}`; } - let helpTip = ''; - if (showHelpTip && (instructions === undefined || instructions)) { + let helpTipTop = ''; + let helpTipBottom = ''; + if ( + theme.helpMode === 'always' || + (theme.helpMode === 'auto' && + showHelpTip && + (instructions === undefined || instructions)) + ) { if (typeof instructions === 'string') { - helpTip = instructions; + helpTipTop = instructions; } else { const keys = [ `${theme.style.key('space')} to select`, @@ -218,16 +227,25 @@ export default createPrompt( `${theme.style.key('i')} to invert selection`, `and ${theme.style.key('enter')} to proceed`, ]; - helpTip = ` (Press ${keys.join(', ')})`; + helpTipTop = ` (Press ${keys.join(', ')})`; + } + + if ( + items.length > pageSize && + (theme.helpMode === 'always' || + (theme.helpMode === 'auto' && firstRender.current)) + ) { + helpTipBottom = `\n${theme.style.help('(Use arrow keys to reveal more choices)')}`; + firstRender.current = false; } } let error = ''; if (errorMsg) { - error = theme.style.error(errorMsg); + error = `\n${theme.style.error(errorMsg)}`; } - return `${prefix} ${message}${helpTip}\n${page}\n${error}${ansiEscapes.cursorHide}`; + return `${prefix} ${message}${helpTipTop}\n${page}${helpTipBottom}${error}${ansiEscapes.cursorHide}`; }, ); diff --git a/packages/core/README.md b/packages/core/README.md index bd1438223..90e11cc96 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -141,7 +141,6 @@ export default createPrompt((config, done) => { renderItem: ({ item, index, isActive }) => `${isActive ? ">" : " "}${index}. ${item.toString()}` pageSize: config.pageSize, loop: config.loop, - theme, config.theme, }); return `... ${page}`; diff --git a/packages/core/src/lib/pagination/use-pagination.mts b/packages/core/src/lib/pagination/use-pagination.mts index fa9d86431..f004c68f1 100644 --- a/packages/core/src/lib/pagination/use-pagination.mts +++ b/packages/core/src/lib/pagination/use-pagination.mts @@ -1,7 +1,6 @@ import type { Prettify } from '@inquirer/type'; import { useRef } from '../use-ref.mjs'; import { readlineWidth } from '../utils.mjs'; -import { makeTheme } from '../make-theme.mjs'; import { type Theme } from '../theme.mjs'; import { lines, type Layout } from './lines.mjs'; import { finite, infinite } from './position.mjs'; @@ -12,7 +11,6 @@ export function usePagination({ renderItem, pageSize, loop = true, - theme: defaultTheme, }: { items: readonly T[]; /** The index of the active item. */ @@ -26,7 +24,6 @@ export function usePagination({ theme?: Theme; }): string { const state = useRef({ position: 0, lastActive: 0 }); - const theme = makeTheme(defaultTheme); const position = loop ? infinite({ @@ -45,7 +42,7 @@ export function usePagination({ state.current.position = position; state.current.lastActive = active; - const visibleLines = lines({ + return lines({ items, width: readlineWidth(), renderItem, @@ -53,10 +50,4 @@ export function usePagination({ position, pageSize, }).join('\n'); - - if (items.length > pageSize) { - return `${visibleLines}\n${theme.style.help('(Use arrow keys to reveal more choices)')}`; - } - - return visibleLines; } diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 80a090a31..d8c31e693 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -85,5 +85,8 @@ "@inquirer/rawlist": "^2.1.3", "@inquirer/select": "^2.2.3" }, + "devDependencies": { + "@inquirer/type": "^1.2.1" + }, "homepage": "https://github.com/SBoudrias/Inquirer.js/blob/master/packages/prompts/README.md" } diff --git a/packages/prompts/prompts.test.mts b/packages/prompts/prompts.test.mts index 0a550aac9..114583968 100644 --- a/packages/prompts/prompts.test.mts +++ b/packages/prompts/prompts.test.mts @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import { Expect, Equal } from '@inquirer/type'; import { checkbox, confirm, @@ -22,3 +23,13 @@ describe('@inquirer/prompts', () => { expect(select).toBeTypeOf('function'); }); }); + +/** + * Type assertions to validate the interfaces. + */ +Expect< + Equal< + Parameters[0]['theme']['helpMode'], + Parameters[0]['theme']['helpMode'] + > +>; diff --git a/packages/select/README.md b/packages/select/README.md index b237f8876..a51a65ffd 100644 --- a/packages/select/README.md +++ b/packages/select/README.md @@ -80,9 +80,16 @@ type Theme = { icon: { cursor: string; }; + helpMode: 'always' | 'never' | 'auto'; }; ``` +### `theme.helpMode` + +- `auto` (default): Hide the help tips after an interaction occurs. +- `always`: The help tips will always show and never hide. +- `never`: The help tips will never show. + # License Copyright (c) 2023 Simon Boudrias (twitter: [@vaxilart](https://twitter.com/Vaxilart))
diff --git a/packages/select/select.test.mts b/packages/select/select.test.mts index 95ede1dac..b3f09b26e 100644 --- a/packages/select/select.test.mts +++ b/packages/select/select.test.mts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, afterEach } from 'vitest'; import { render } from '@inquirer/testing'; import { ValidationError } from '@inquirer/core'; import select, { Separator } from './src/index.mjs'; @@ -18,6 +18,10 @@ const numberedChoices = [ { value: 12 }, ]; +afterEach(() => { + vi.useRealTimers(); +}); + describe('select prompt', () => { it('use arrow keys to select an option', async () => { const { answer, events, getScreen } = await render(select, { @@ -47,8 +51,7 @@ describe('select prompt', () => { 4 5 6 - 7 - (Use arrow keys to reveal more choices)" + 7" `); events.keypress('enter'); @@ -91,8 +94,7 @@ describe('select prompt', () => { ❯ 4 5 6 - 7 - (Use arrow keys to reveal more choices)" + 7" `); events.keypress('enter'); @@ -164,8 +166,7 @@ describe('select prompt', () => { expect(getScreen()).toMatchInlineSnapshot(` "? Select a number ❯ 11 - 12 - (Use arrow keys to reveal more choices)" + 12" `); events.keypress('enter'); @@ -248,8 +249,7 @@ describe('select prompt', () => { expect(getScreen()).toMatchInlineSnapshot(` "? Select a number 11 - ❯ 12 - (Use arrow keys to reveal more choices)" + ❯ 12" `); events.keypress('enter'); @@ -278,8 +278,7 @@ describe('select prompt', () => { "? Select a number 11 ❯ 12 - ────────────── - (Use arrow keys to reveal more choices)" + ──────────────" `); events.keypress('enter'); @@ -560,4 +559,122 @@ describe('select prompt', () => { vi.runAllTimers(); await expect(answer).resolves.toEqual('US'); }); + + describe('theme: helpMode', () => { + const scrollTip = '(Use arrow keys to reveal more choices)'; + + it('helpMode: auto', async () => { + const { answer, events, getScreen } = await render(select, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'auto' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯ 1 + 2 + 3 + 4 + 5 + 6 + 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + 1 + ❯ 2 + 3 + 4 + 5 + 6 + 7" + `); + expect(getScreen()).not.toContain(scrollTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual(2); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number 2"'); + }); + + it('helpMode: always', async () => { + const { answer, events, getScreen } = await render(select, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'always' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯ 1 + 2 + 3 + 4 + 5 + 6 + 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + 1 + ❯ 2 + 3 + 4 + 5 + 6 + 7 + (Use arrow keys to reveal more choices)" + `); + expect(getScreen()).toContain(scrollTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual(2); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number 2"'); + }); + + it('helpMode: never', async () => { + const { answer, events, getScreen } = await render(select, { + message: 'Select a number', + choices: numberedChoices, + theme: { helpMode: 'never' }, + }); + + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + ❯ 1 + 2 + 3 + 4 + 5 + 6 + 7" + `); + expect(getScreen()).not.toContain(scrollTip); + + events.keypress('down'); + expect(getScreen()).toMatchInlineSnapshot(` + "? Select a number + 1 + ❯ 2 + 3 + 4 + 5 + 6 + 7" + `); + expect(getScreen()).not.toContain(scrollTip); + + events.keypress('enter'); + await expect(answer).resolves.toEqual(2); + expect(getScreen()).toMatchInlineSnapshot('"? Select a number 2"'); + }); + }); }); diff --git a/packages/select/src/index.mts b/packages/select/src/index.mts index 0e2201a4b..1b545186b 100644 --- a/packages/select/src/index.mts +++ b/packages/select/src/index.mts @@ -24,11 +24,13 @@ import ansiEscapes from 'ansi-escapes'; type SelectTheme = { icon: { cursor: string }; style: { disabled: (text: string) => string }; + helpMode: 'always' | 'never' | 'auto'; }; const selectTheme: SelectTheme = { icon: { cursor: figures.pointer }, style: { disabled: (text: string) => chalk.dim(`- ${text}`) }, + helpMode: 'auto', }; type Choice = { @@ -142,10 +144,19 @@ export default createPrompt( const message = theme.style.message(config.message); - let helpTip; - if (firstRender.current && items.length <= pageSize) { + let helpTipTop = ''; + let helpTipBottom = ''; + if ( + theme.helpMode === 'always' || + (theme.helpMode === 'auto' && firstRender.current) + ) { firstRender.current = false; - helpTip = theme.style.help('(Use arrow keys)'); + + if (items.length > pageSize) { + helpTipBottom = `\n${theme.style.help('(Use arrow keys to reveal more choices)')}`; + } else { + helpTipTop = theme.style.help('(Use arrow keys)'); + } } const page = usePagination>({ @@ -169,7 +180,6 @@ export default createPrompt( }, pageSize, loop, - theme, }); if (status === 'done') { @@ -184,7 +194,7 @@ export default createPrompt( ? `\n${selectedChoice.description}` : ``; - return `${[prefix, message, helpTip].filter(Boolean).join(' ')}\n${page}${choiceDescription}${ansiEscapes.cursorHide}`; + return `${[prefix, message, helpTipTop].filter(Boolean).join(' ')}\n${page}${choiceDescription}${helpTipBottom}${ansiEscapes.cursorHide}`; }, ); diff --git a/packages/type/src/index.mts b/packages/type/src/index.mts index ecd7f091c..f72018725 100644 --- a/packages/type/src/index.mts +++ b/packages/type/src/index.mts @@ -22,3 +22,25 @@ export type Prompt = ( config: Config, context?: Context, ) => CancelablePromise; + +/** + * Utility types used for writing tests + * + * Equal checks that A and B are the same type, and returns + * either `true` or `false`. + * + * You can use it in combination with `Expect` to write type + * inference unit tests: + * + * ```ts + * type t = Expect< + * Equal, { a?: string }> + * > + * ``` + */ +export type Equal = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; + +export type Expect = T; + +export type Not = T extends true ? false : true; diff --git a/yarn.lock b/yarn.lock index 22e8cf583..66afd8c95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -504,6 +504,7 @@ __metadata: "@inquirer/password": "npm:^2.1.3" "@inquirer/rawlist": "npm:^2.1.3" "@inquirer/select": "npm:^2.2.3" + "@inquirer/type": "npm:^1.2.1" languageName: unknown linkType: soft @@ -544,7 +545,7 @@ __metadata: languageName: unknown linkType: soft -"@inquirer/type@npm:^1.2.2, @inquirer/type@workspace:packages/type": +"@inquirer/type@npm:^1.2.1, @inquirer/type@npm:^1.2.2, @inquirer/type@workspace:packages/type": version: 0.0.0-use.local resolution: "@inquirer/type@workspace:packages/type" languageName: unknown