Skip to content

Commit 7530af0

Browse files
authored
fix: wrap messages in select prompts (#410)
1 parent 38019c7 commit 7530af0

File tree

7 files changed

+230
-10
lines changed

7 files changed

+230
-10
lines changed

.changeset/plenty-snakes-ring.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 wrapping of cancelled and success messages of select prompt

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ export { default as SelectPrompt } from './prompts/select.js';
88
export { default as SelectKeyPrompt } from './prompts/select-key.js';
99
export { default as TextPrompt } from './prompts/text.js';
1010
export type { ClackState as State } from './types.js';
11-
export { block, getColumns, getRows, isCancel } from './utils/index.js';
11+
export { block, getColumns, getRows, isCancel, wrapTextWithPrefix } from './utils/index.js';
1212
export type { ClackSettings } from './utils/settings.js';
1313
export { settings, updateSettings } from './utils/settings.js';

packages/core/src/utils/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Key } from 'node:readline';
33
import * as readline from 'node:readline';
44
import type { Readable, Writable } from 'node:stream';
55
import { ReadStream } from 'node:tty';
6+
import { wrapAnsi } from 'fast-wrap-ansi';
67
import { cursor } from 'sisteransi';
78
import { isActionKey } from './settings.js';
89

@@ -96,3 +97,23 @@ export const getRows = (output: Writable): number => {
9697
}
9798
return 20;
9899
};
100+
101+
export function wrapTextWithPrefix(
102+
output: Writable | undefined,
103+
text: string,
104+
prefix: string,
105+
startPrefix: string = prefix
106+
): string {
107+
const columns = getColumns(output ?? stdout);
108+
const wrapped = wrapAnsi(text, columns - prefix.length, {
109+
hard: true,
110+
trim: false,
111+
});
112+
const lines = wrapped
113+
.split('\n')
114+
.map((line, index) => {
115+
return `${index === 0 ? startPrefix : prefix}${line}`;
116+
})
117+
.join('\n');
118+
return lines;
119+
}

packages/prompts/src/common.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ export const symbol = (state: State) => {
5353
}
5454
};
5555

56+
export const symbolBar = (state: State) => {
57+
switch (state) {
58+
case 'initial':
59+
case 'active':
60+
return color.cyan(S_BAR);
61+
case 'cancel':
62+
return color.red(S_BAR);
63+
case 'error':
64+
return color.yellow(S_BAR);
65+
case 'submit':
66+
return color.green(S_BAR);
67+
}
68+
};
69+
5670
export interface CommonOptions {
5771
input?: Readable;
5872
output?: Writable;

packages/prompts/src/select.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SelectPrompt } from '@clack/core';
1+
import { SelectPrompt, wrapTextWithPrefix } from '@clack/core';
22
import color from 'picocolors';
33
import {
44
type CommonOptions,
@@ -7,6 +7,7 @@ import {
77
S_RADIO_ACTIVE,
88
S_RADIO_INACTIVE,
99
symbol,
10+
symbolBar,
1011
} from './common.js';
1112
import { limitOptions } from './limit-options.js';
1213

@@ -102,16 +103,35 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
102103
output: opts.output,
103104
initialValue: opts.initialValue,
104105
render() {
105-
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
106+
const titlePrefix = `${symbol(this.state)} `;
107+
const titlePrefixBar = `${symbolBar(this.state)} `;
108+
const messageLines = wrapTextWithPrefix(
109+
opts.output,
110+
opts.message,
111+
titlePrefixBar,
112+
titlePrefix
113+
);
114+
const title = `${color.gray(S_BAR)}\n${messageLines}\n`;
106115

107116
switch (this.state) {
108-
case 'submit':
109-
return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`;
110-
case 'cancel':
111-
return `${title}${color.gray(S_BAR)} ${opt(
112-
this.options[this.cursor],
113-
'cancelled'
114-
)}\n${color.gray(S_BAR)}`;
117+
case 'submit': {
118+
const submitPrefix = `${color.gray(S_BAR)} `;
119+
const wrappedLines = wrapTextWithPrefix(
120+
opts.output,
121+
opt(this.options[this.cursor], 'selected'),
122+
submitPrefix
123+
);
124+
return `${title}${wrappedLines}`;
125+
}
126+
case 'cancel': {
127+
const cancelPrefix = `${color.gray(S_BAR)} `;
128+
const wrappedLines = wrapTextWithPrefix(
129+
opts.output,
130+
opt(this.options[this.cursor], 'cancelled'),
131+
cancelPrefix
132+
);
133+
return `${title}${wrappedLines}\n${color.gray(S_BAR)}`;
134+
}
115135
default: {
116136
const prefix = `${color.cyan(S_BAR)} `;
117137
return `${title}${prefix}${limitOptions({

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,63 @@ exports[`select (isCI = false) > up arrow selects previous option 1`] = `
178178
]
179179
`;
180180
181+
exports[`select (isCI = false) > wraps long cancelled message 1`] = `
182+
[
183+
"<cursor.hide>",
184+
"│
185+
◆ foo
186+
│ ● foo foo foo foo foo foo
187+
│ foo foo foo foo foo foo foo
188+
│ foo foo foo foo foo foo
189+
│ foo foo foo foo foo foo foo
190+
│ foo foo foo foo
191+
│ ○ Option 1
192+
└
193+
",
194+
"<cursor.backward count=999><cursor.up count=9>",
195+
"<cursor.down count=1>",
196+
"<erase.down>",
197+
"■ foo
198+
│ foo foo foo foo foo foo foo
199+
│  foo foo foo foo foo foo 
200+
│ foo foo foo foo foo foo foo
201+
│  foo foo foo foo foo foo 
202+
│ foo foo foo foo
203+
│",
204+
"
205+
",
206+
"<cursor.show>",
207+
]
208+
`;
209+
210+
exports[`select (isCI = false) > wraps long results 1`] = `
211+
[
212+
"<cursor.hide>",
213+
"│
214+
◆ foo
215+
│ ● foo foo foo foo foo foo
216+
│ foo foo foo foo foo foo foo
217+
│ foo foo foo foo foo foo
218+
│ foo foo foo foo foo foo foo
219+
│ foo foo foo foo
220+
│ ○ Option 1
221+
└
222+
",
223+
"<cursor.backward count=999><cursor.up count=9>",
224+
"<cursor.down count=1>",
225+
"<erase.down>",
226+
"◇ foo
227+
│ foo foo foo foo foo foo foo
228+
│  foo foo foo foo foo foo 
229+
│ foo foo foo foo foo foo foo
230+
│  foo foo foo foo foo foo 
231+
│ foo foo foo foo",
232+
"
233+
",
234+
"<cursor.show>",
235+
]
236+
`;
237+
181238
exports[`select (isCI = true) > can be aborted by a signal 1`] = `
182239
[
183240
"<cursor.hide>",
@@ -355,3 +412,60 @@ exports[`select (isCI = true) > up arrow selects previous option 1`] = `
355412
"<cursor.show>",
356413
]
357414
`;
415+
416+
exports[`select (isCI = true) > wraps long cancelled message 1`] = `
417+
[
418+
"<cursor.hide>",
419+
"│
420+
◆ foo
421+
│ ● foo foo foo foo foo foo
422+
│ foo foo foo foo foo foo foo
423+
│ foo foo foo foo foo foo
424+
│ foo foo foo foo foo foo foo
425+
│ foo foo foo foo
426+
│ ○ Option 1
427+
└
428+
",
429+
"<cursor.backward count=999><cursor.up count=9>",
430+
"<cursor.down count=1>",
431+
"<erase.down>",
432+
"■ foo
433+
│ foo foo foo foo foo foo foo
434+
│  foo foo foo foo foo foo 
435+
│ foo foo foo foo foo foo foo
436+
│  foo foo foo foo foo foo 
437+
│ foo foo foo foo
438+
│",
439+
"
440+
",
441+
"<cursor.show>",
442+
]
443+
`;
444+
445+
exports[`select (isCI = true) > wraps long results 1`] = `
446+
[
447+
"<cursor.hide>",
448+
"│
449+
◆ foo
450+
│ ● foo foo foo foo foo foo
451+
│ foo foo foo foo foo foo foo
452+
│ foo foo foo foo foo foo
453+
│ foo foo foo foo foo foo foo
454+
│ foo foo foo foo
455+
│ ○ Option 1
456+
└
457+
",
458+
"<cursor.backward count=999><cursor.up count=9>",
459+
"<cursor.down count=1>",
460+
"<erase.down>",
461+
"◇ foo
462+
│ foo foo foo foo foo foo foo
463+
│  foo foo foo foo foo foo 
464+
│ foo foo foo foo foo foo foo
465+
│  foo foo foo foo foo foo 
466+
│ foo foo foo foo",
467+
"
468+
",
469+
"<cursor.show>",
470+
]
471+
`;

packages/prompts/test/select.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,50 @@ describe.each(['true', 'false'])('select (isCI = %s)', (isCI) => {
165165
expect(value).toBe('opt1');
166166
expect(output.buffer).toMatchSnapshot();
167167
});
168+
169+
test('wraps long results', async () => {
170+
output.columns = 40;
171+
172+
const result = prompts.select({
173+
message: 'foo',
174+
options: [
175+
{
176+
value: 'opt0',
177+
label: 'foo '.repeat(30).trim(),
178+
},
179+
{ value: 'opt1', label: 'Option 1' },
180+
],
181+
input,
182+
output,
183+
});
184+
185+
input.emit('keypress', '', { name: 'return' });
186+
187+
await result;
188+
189+
expect(output.buffer).toMatchSnapshot();
190+
});
191+
192+
test('wraps long cancelled message', async () => {
193+
output.columns = 40;
194+
195+
const result = prompts.select({
196+
message: 'foo',
197+
options: [
198+
{
199+
value: 'opt0',
200+
label: 'foo '.repeat(30).trim(),
201+
},
202+
{ value: 'opt1', label: 'Option 1' },
203+
],
204+
input,
205+
output,
206+
});
207+
208+
input.emit('keypress', 'escape', { name: 'escape' });
209+
210+
await result;
211+
212+
expect(output.buffer).toMatchSnapshot();
213+
});
168214
});

0 commit comments

Comments
 (0)