diff --git a/.changeset/busy-baths-work.md b/.changeset/busy-baths-work.md new file mode 100644 index 00000000..5debdede --- /dev/null +++ b/.changeset/busy-baths-work.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": patch +--- + +Fixes rendering of multi-line messages and options in select prompt. diff --git a/packages/prompts/src/select.ts b/packages/prompts/src/select.ts index 466ee0f5..b6bb6721 100644 --- a/packages/prompts/src/select.ts +++ b/packages/prompts/src/select.ts @@ -72,6 +72,16 @@ export interface SelectOptions extends CommonOptions { maxItems?: number; } +const computeLabel = (label: string, format: (text: string) => string) => { + if (!label.includes('\n')) { + return format(label); + } + return label + .split('\n') + .map((line) => format(line)) + .join('\n'); +}; + export const select = (opts: SelectOptions) => { const opt = ( option: Option, @@ -80,19 +90,19 @@ export const select = (opts: SelectOptions) => { const label = option.label ?? String(option.value); switch (state) { case 'disabled': - return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${ + return `${color.gray(S_RADIO_INACTIVE)} ${computeLabel(label, color.gray)}${ option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' }`; case 'selected': - return `${color.dim(label)}`; + return `${computeLabel(label, color.dim)}`; case 'active': return `${color.green(S_RADIO_ACTIVE)} ${label}${ option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' }`; case 'cancelled': - return `${color.strikethrough(color.dim(label))}`; + return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`; default: - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S_RADIO_INACTIVE)} ${computeLabel(label, color.dim)}`; } }; diff --git a/packages/prompts/test/__snapshots__/select.test.ts.snap b/packages/prompts/test/__snapshots__/select.test.ts.snap index 805c5f63..cd5e3415 100644 --- a/packages/prompts/test/__snapshots__/select.test.ts.snap +++ b/packages/prompts/test/__snapshots__/select.test.ts.snap @@ -84,6 +84,35 @@ exports[`select (isCI = false) > renders disabled options 1`] = ` ] `; +exports[`select (isCI = false) > renders multi-line option labels 1`] = ` +[ + "", + "│ +◆ foo +│ ● Option 0 +│ with multiple lines +│ ○ Option 1 +└ +", + "", + "", + "", + "│ ○ Option 0 +│ with multiple lines +│ ● Option 1 +└ +", + "", + "", + "", + "◇ foo +│ Option 1", + " +", + "", +] +`; + exports[`select (isCI = false) > renders option hints 1`] = ` [ "", @@ -207,6 +236,30 @@ exports[`select (isCI = false) > wraps long cancelled message 1`] = ` ] `; +exports[`select (isCI = false) > wraps long messages 1`] = ` +[ + "", + "│ +◆ foo foo foo foo foo foo foo +│ foo foo foo foo foo foo +│ foo foo foo foo foo foo foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo foo foo foo foo foo foo +│ foo foo foo foo foo foo +│ foo foo foo foo foo foo foo +│ opt0", + " +", + "", +] +`; + exports[`select (isCI = false) > wraps long results 1`] = ` [ "", @@ -319,6 +372,35 @@ exports[`select (isCI = true) > renders disabled options 1`] = ` ] `; +exports[`select (isCI = true) > renders multi-line option labels 1`] = ` +[ + "", + "│ +◆ foo +│ ● Option 0 +│ with multiple lines +│ ○ Option 1 +└ +", + "", + "", + "", + "│ ○ Option 0 +│ with multiple lines +│ ● Option 1 +└ +", + "", + "", + "", + "◇ foo +│ Option 1", + " +", + "", +] +`; + exports[`select (isCI = true) > renders option hints 1`] = ` [ "", @@ -442,6 +524,30 @@ exports[`select (isCI = true) > wraps long cancelled message 1`] = ` ] `; +exports[`select (isCI = true) > wraps long messages 1`] = ` +[ + "", + "│ +◆ foo foo foo foo foo foo foo +│ foo foo foo foo foo foo +│ foo foo foo foo foo foo foo +│ ● opt0 +│ ○ opt1 +└ +", + "", + "", + "", + "◇ foo foo foo foo foo foo foo +│ foo foo foo foo foo foo +│ foo foo foo foo foo foo foo +│ opt0", + " +", + "", +] +`; + exports[`select (isCI = true) > wraps long results 1`] = ` [ "", diff --git a/packages/prompts/test/select.test.ts b/packages/prompts/test/select.test.ts index 10b73dc5..6347ad17 100644 --- a/packages/prompts/test/select.test.ts +++ b/packages/prompts/test/select.test.ts @@ -211,4 +211,41 @@ describe.each(['true', 'false'])('select (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + + test('wraps long messages', async () => { + output.columns = 40; + + const result = prompts.select({ + message: 'foo '.repeat(20).trim(), + options: [{ value: 'opt0' }, { value: 'opt1' }], + input, + output, + }); + + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toEqual('opt0'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('renders multi-line option labels', async () => { + const result = prompts.select({ + message: 'foo', + options: [ + { value: 'opt0', label: 'Option 0\nwith multiple lines' }, + { value: 'opt1', label: 'Option 1' }, + ], + input, + output, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); });