Skip to content

Commit 4d1d83b

Browse files
authored
fix: handle multi-line select options (#413)
1 parent b0fa7d8 commit 4d1d83b

File tree

4 files changed

+162
-4
lines changed

4 files changed

+162
-4
lines changed

.changeset/busy-baths-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": patch
3+
---
4+
5+
Fixes rendering of multi-line messages and options in select prompt.

packages/prompts/src/select.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ export interface SelectOptions<Value> extends CommonOptions {
7272
maxItems?: number;
7373
}
7474

75+
const computeLabel = (label: string, format: (text: string) => string) => {
76+
if (!label.includes('\n')) {
77+
return format(label);
78+
}
79+
return label
80+
.split('\n')
81+
.map((line) => format(line))
82+
.join('\n');
83+
};
84+
7585
export const select = <Value>(opts: SelectOptions<Value>) => {
7686
const opt = (
7787
option: Option<Value>,
@@ -80,19 +90,19 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
8090
const label = option.label ?? String(option.value);
8191
switch (state) {
8292
case 'disabled':
83-
return `${color.gray(S_RADIO_INACTIVE)} ${color.gray(label)}${
93+
return `${color.gray(S_RADIO_INACTIVE)} ${computeLabel(label, color.gray)}${
8494
option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : ''
8595
}`;
8696
case 'selected':
87-
return `${color.dim(label)}`;
97+
return `${computeLabel(label, color.dim)}`;
8898
case 'active':
8999
return `${color.green(S_RADIO_ACTIVE)} ${label}${
90100
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
91101
}`;
92102
case 'cancelled':
93-
return `${color.strikethrough(color.dim(label))}`;
103+
return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`;
94104
default:
95-
return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`;
105+
return `${color.dim(S_RADIO_INACTIVE)} ${computeLabel(label, color.dim)}`;
96106
}
97107
};
98108

packages/prompts/test/__snapshots__/select.test.ts.snap

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ exports[`select (isCI = false) > renders disabled options 1`] = `
8484
]
8585
`;
8686
87+
exports[`select (isCI = false) > renders multi-line option labels 1`] = `
88+
[
89+
"<cursor.hide>",
90+
"│
91+
◆ foo
92+
│ ● Option 0
93+
│ with multiple lines
94+
│ ○ Option 1
95+
└
96+
",
97+
"<cursor.backward count=999><cursor.up count=6>",
98+
"<cursor.down count=2>",
99+
"<erase.down>",
100+
"│ ○ Option 0
101+
│ with multiple lines
102+
│ ● Option 1
103+
└
104+
",
105+
"<cursor.backward count=999><cursor.up count=6>",
106+
"<cursor.down count=1>",
107+
"<erase.down>",
108+
"◇ foo
109+
│ Option 1",
110+
"
111+
",
112+
"<cursor.show>",
113+
]
114+
`;
115+
87116
exports[`select (isCI = false) > renders option hints 1`] = `
88117
[
89118
"<cursor.hide>",
@@ -207,6 +236,30 @@ exports[`select (isCI = false) > wraps long cancelled message 1`] = `
207236
]
208237
`;
209238
239+
exports[`select (isCI = false) > wraps long messages 1`] = `
240+
[
241+
"<cursor.hide>",
242+
"│
243+
◆ foo foo foo foo foo foo foo
244+
│ foo foo foo foo foo foo
245+
│ foo foo foo foo foo foo foo
246+
│ ● opt0
247+
│ ○ opt1
248+
└
249+
",
250+
"<cursor.backward count=999><cursor.up count=7>",
251+
"<cursor.down count=1>",
252+
"<erase.down>",
253+
"◇ foo foo foo foo foo foo foo
254+
│ foo foo foo foo foo foo
255+
│ foo foo foo foo foo foo foo
256+
│ opt0",
257+
"
258+
",
259+
"<cursor.show>",
260+
]
261+
`;
262+
210263
exports[`select (isCI = false) > wraps long results 1`] = `
211264
[
212265
"<cursor.hide>",
@@ -319,6 +372,35 @@ exports[`select (isCI = true) > renders disabled options 1`] = `
319372
]
320373
`;
321374
375+
exports[`select (isCI = true) > renders multi-line option labels 1`] = `
376+
[
377+
"<cursor.hide>",
378+
"│
379+
◆ foo
380+
│ ● Option 0
381+
│ with multiple lines
382+
│ ○ Option 1
383+
└
384+
",
385+
"<cursor.backward count=999><cursor.up count=6>",
386+
"<cursor.down count=2>",
387+
"<erase.down>",
388+
"│ ○ Option 0
389+
│ with multiple lines
390+
│ ● Option 1
391+
└
392+
",
393+
"<cursor.backward count=999><cursor.up count=6>",
394+
"<cursor.down count=1>",
395+
"<erase.down>",
396+
"◇ foo
397+
│ Option 1",
398+
"
399+
",
400+
"<cursor.show>",
401+
]
402+
`;
403+
322404
exports[`select (isCI = true) > renders option hints 1`] = `
323405
[
324406
"<cursor.hide>",
@@ -442,6 +524,30 @@ exports[`select (isCI = true) > wraps long cancelled message 1`] = `
442524
]
443525
`;
444526
527+
exports[`select (isCI = true) > wraps long messages 1`] = `
528+
[
529+
"<cursor.hide>",
530+
"│
531+
◆ foo foo foo foo foo foo foo
532+
│ foo foo foo foo foo foo
533+
│ foo foo foo foo foo foo foo
534+
│ ● opt0
535+
│ ○ opt1
536+
└
537+
",
538+
"<cursor.backward count=999><cursor.up count=7>",
539+
"<cursor.down count=1>",
540+
"<erase.down>",
541+
"◇ foo foo foo foo foo foo foo
542+
│ foo foo foo foo foo foo
543+
│ foo foo foo foo foo foo foo
544+
│ opt0",
545+
"
546+
",
547+
"<cursor.show>",
548+
]
549+
`;
550+
445551
exports[`select (isCI = true) > wraps long results 1`] = `
446552
[
447553
"<cursor.hide>",

packages/prompts/test/select.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,41 @@ describe.each(['true', 'false'])('select (isCI = %s)', (isCI) => {
211211

212212
expect(output.buffer).toMatchSnapshot();
213213
});
214+
215+
test('wraps long messages', async () => {
216+
output.columns = 40;
217+
218+
const result = prompts.select({
219+
message: 'foo '.repeat(20).trim(),
220+
options: [{ value: 'opt0' }, { value: 'opt1' }],
221+
input,
222+
output,
223+
});
224+
225+
input.emit('keypress', '', { name: 'return' });
226+
227+
const value = await result;
228+
229+
expect(value).toEqual('opt0');
230+
expect(output.buffer).toMatchSnapshot();
231+
});
232+
233+
test('renders multi-line option labels', async () => {
234+
const result = prompts.select({
235+
message: 'foo',
236+
options: [
237+
{ value: 'opt0', label: 'Option 0\nwith multiple lines' },
238+
{ value: 'opt1', label: 'Option 1' },
239+
],
240+
input,
241+
output,
242+
});
243+
244+
input.emit('keypress', '', { name: 'down' });
245+
input.emit('keypress', '', { name: 'return' });
246+
247+
await result;
248+
249+
expect(output.buffer).toMatchSnapshot();
250+
});
214251
});

0 commit comments

Comments
 (0)