Skip to content

Commit 50f614a

Browse files
committed
merge upstream
2 parents d194252 + 4ba2d78 commit 50f614a

File tree

7 files changed

+321
-28
lines changed

7 files changed

+321
-28
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.

.changeset/little-ghosts-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/core": patch
3+
---
4+
5+
Support short terminal windows when re-rendering by accounting for off-screen lines

packages/core/src/prompts/prompt.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import { wrapAnsi } from 'fast-wrap-ansi';
55
import { cursor, erase } from 'sisteransi';
66
import type { ClackEvents, ClackState } from '../types.js';
77
import type { Action } from '../utils/index.js';
8-
import { CANCEL_SYMBOL, diffLines, isActionKey, setRawMode, settings } from '../utils/index.js';
8+
import {
9+
CANCEL_SYMBOL,
10+
diffLines,
11+
getRows,
12+
isActionKey,
13+
setRawMode,
14+
settings,
15+
} from '../utils/index.js';
916

1017
export interface PromptOptions<TValue, Self extends Prompt<TValue>> {
1118
render(this: Omit<Self, 'prompt'>): string | undefined;
@@ -274,28 +281,44 @@ export default class Prompt<TValue> {
274281
this.output.write(cursor.hide);
275282
} else {
276283
const diff = diffLines(this._prevFrame, frame);
284+
const rows = getRows(this.output);
277285
this.restoreCursor();
278-
// If a single line has changed, only update that line
279-
if (diff && diff?.length === 1) {
280-
const diffLine = diff[0];
281-
this.output.write(cursor.move(0, diffLine));
282-
this.output.write(erase.lines(1));
283-
const lines = frame.split('\n');
284-
this.output.write(lines[diffLine]);
285-
this._prevFrame = frame;
286-
this.output.write(cursor.move(0, lines.length - diffLine - 1));
287-
return;
288-
// If many lines have changed, rerender everything past the first line
289-
}
290-
if (diff && diff?.length > 1) {
291-
const diffLine = diff[0];
292-
this.output.write(cursor.move(0, diffLine));
293-
this.output.write(erase.down());
294-
const lines = frame.split('\n');
295-
const newLines = lines.slice(diffLine);
296-
this.output.write(newLines.join('\n'));
297-
this._prevFrame = frame;
298-
return;
286+
if (diff) {
287+
const diffOffsetAfter = Math.max(0, diff.numLinesAfter - rows);
288+
const diffOffsetBefore = Math.max(0, diff.numLinesBefore - rows);
289+
let diffLine = diff.lines.find((line) => line >= diffOffsetAfter);
290+
291+
if (diffLine === undefined) {
292+
this._prevFrame = frame;
293+
return;
294+
}
295+
296+
// If a single line has changed, only update that line
297+
if (diff.lines.length === 1) {
298+
this.output.write(cursor.move(0, diffLine - diffOffsetBefore));
299+
this.output.write(erase.lines(1));
300+
const lines = frame.split('\n');
301+
this.output.write(lines[diffLine]);
302+
this._prevFrame = frame;
303+
this.output.write(cursor.move(0, lines.length - diffLine - 1));
304+
return;
305+
// If many lines have changed, rerender everything past the first line
306+
} else if (diff.lines.length > 1) {
307+
if (diffOffsetAfter < diffOffsetBefore) {
308+
diffLine = diffOffsetAfter;
309+
} else {
310+
const adjustedDiffLine = diffLine - diffOffsetBefore;
311+
if (adjustedDiffLine > 0) {
312+
this.output.write(cursor.move(0, adjustedDiffLine));
313+
}
314+
}
315+
this.output.write(erase.down());
316+
const lines = frame.split('\n');
317+
const newLines = lines.slice(diffLine);
318+
this.output.write(newLines.join('\n'));
319+
this._prevFrame = frame;
320+
return;
321+
}
299322
}
300323

301324
this.output.write(erase.down());

packages/core/src/utils/string.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ export function diffLines(a: string, b: string) {
33

44
const aLines = a.split('\n');
55
const bLines = b.split('\n');
6+
const numLines = Math.max(aLines.length, bLines.length);
67
const diff: number[] = [];
78

8-
for (let i = 0; i < Math.max(aLines.length, bLines.length); i++) {
9+
for (let i = 0; i < numLines; i++) {
910
if (aLines[i] !== bLines[i]) diff.push(i);
1011
}
1112

12-
return diff;
13+
return {
14+
lines: diff,
15+
numLinesBefore: aLines.length,
16+
numLinesAfter: bLines.length,
17+
numLines,
18+
};
1319
}

packages/prompts/src/select.ts

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

67+
const computeLabel = (label: string, format: (text: string) => string) => {
68+
if (!label.includes('\n')) {
69+
return format(label);
70+
}
71+
return label
72+
.split('\n')
73+
.map((line) => format(line))
74+
.join('\n');
75+
};
76+
6777
export const select = <Value>(opts: SelectOptions<Value>) => {
6878
const style = extendStyle(opts.theme);
6979
const opt = (
@@ -73,19 +83,19 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
7383
const label = option.label ?? String(option.value);
7484
switch (state) {
7585
case 'disabled':
76-
return `${style.radio.disabled} ${color.gray(label)}${
86+
return `${style.radio.disabled} ${computeLabel(label, color.gray)}${
7787
option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : ''
7888
}`;
7989
case 'selected':
80-
return `${color.dim(label)}`;
90+
return `${computeLabel(label, color.dim)}`;
8191
case 'active':
8292
return `${style.radio.active} ${label}${
8393
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
8494
}`;
8595
case 'cancelled':
86-
return `${color.strikethrough(color.dim(label))}`;
96+
return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`;
8797
default:
88-
return `${style.radio.inactive} ${color.dim(label)}`;
98+
return `${style.radio.inactive} ${computeLabel(label, color.dim)}`;
8999
}
90100
};
91101

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

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,43 @@ exports[`select (isCI = false) > down arrow selects next option 1`] = `
6363
]
6464
`;
6565
66+
exports[`select (isCI = false) > handles mixed size re-renders 1`] = `
67+
[
68+
"<cursor.hide>",
69+
"│
70+
◆ Whatever
71+
│ ● Long Option
72+
│ Long Option
73+
│ Long Option
74+
│ Long Option
75+
│ Long Option
76+
│ Long Option
77+
│ Long Option
78+
│ Long Option
79+
│ ...
80+
└
81+
",
82+
"<cursor.backward count=999><cursor.up count=12>",
83+
"<cursor.down count=2>",
84+
"<erase.down>",
85+
"│ ...
86+
│ ○ Option 0
87+
│ ○ Option 1
88+
│ ○ Option 2
89+
│ ● Option 3
90+
└
91+
",
92+
"<cursor.backward count=999><cursor.up count=8>",
93+
"<cursor.down count=1>",
94+
"<erase.down>",
95+
"◇ Whatever
96+
│ Option 3",
97+
"
98+
",
99+
"<cursor.show>",
100+
]
101+
`;
102+
66103
exports[`select (isCI = false) > renders disabled options 1`] = `
67104
[
68105
"<cursor.hide>",
@@ -84,6 +121,35 @@ exports[`select (isCI = false) > renders disabled options 1`] = `
84121
]
85122
`;
86123
124+
exports[`select (isCI = false) > renders multi-line option labels 1`] = `
125+
[
126+
"<cursor.hide>",
127+
"│
128+
◆ foo
129+
│ ● Option 0
130+
│ with multiple lines
131+
│ ○ Option 1
132+
└
133+
",
134+
"<cursor.backward count=999><cursor.up count=6>",
135+
"<cursor.down count=2>",
136+
"<erase.down>",
137+
"│ ○ Option 0
138+
│ with multiple lines
139+
│ ● Option 1
140+
└
141+
",
142+
"<cursor.backward count=999><cursor.up count=6>",
143+
"<cursor.down count=1>",
144+
"<erase.down>",
145+
"◇ foo
146+
│ Option 1",
147+
"
148+
",
149+
"<cursor.show>",
150+
]
151+
`;
152+
87153
exports[`select (isCI = false) > renders option hints 1`] = `
88154
[
89155
"<cursor.hide>",
@@ -207,6 +273,30 @@ exports[`select (isCI = false) > wraps long cancelled message 1`] = `
207273
]
208274
`;
209275
276+
exports[`select (isCI = false) > wraps long messages 1`] = `
277+
[
278+
"<cursor.hide>",
279+
"│
280+
◆ foo foo foo foo foo foo foo
281+
│ foo foo foo foo foo foo
282+
│ foo foo foo foo foo foo foo
283+
│ ● opt0
284+
│ ○ opt1
285+
└
286+
",
287+
"<cursor.backward count=999><cursor.up count=7>",
288+
"<cursor.down count=1>",
289+
"<erase.down>",
290+
"◇ foo foo foo foo foo foo foo
291+
│ foo foo foo foo foo foo
292+
│ foo foo foo foo foo foo foo
293+
│ opt0",
294+
"
295+
",
296+
"<cursor.show>",
297+
]
298+
`;
299+
210300
exports[`select (isCI = false) > wraps long results 1`] = `
211301
[
212302
"<cursor.hide>",
@@ -298,6 +388,43 @@ exports[`select (isCI = true) > down arrow selects next option 1`] = `
298388
]
299389
`;
300390
391+
exports[`select (isCI = true) > handles mixed size re-renders 1`] = `
392+
[
393+
"<cursor.hide>",
394+
"│
395+
◆ Whatever
396+
│ ● Long Option
397+
│ Long Option
398+
│ Long Option
399+
│ Long Option
400+
│ Long Option
401+
│ Long Option
402+
│ Long Option
403+
│ Long Option
404+
│ ...
405+
└
406+
",
407+
"<cursor.backward count=999><cursor.up count=12>",
408+
"<cursor.down count=2>",
409+
"<erase.down>",
410+
"│ ...
411+
│ ○ Option 0
412+
│ ○ Option 1
413+
│ ○ Option 2
414+
│ ● Option 3
415+
└
416+
",
417+
"<cursor.backward count=999><cursor.up count=8>",
418+
"<cursor.down count=1>",
419+
"<erase.down>",
420+
"◇ Whatever
421+
│ Option 3",
422+
"
423+
",
424+
"<cursor.show>",
425+
]
426+
`;
427+
301428
exports[`select (isCI = true) > renders disabled options 1`] = `
302429
[
303430
"<cursor.hide>",
@@ -319,6 +446,35 @@ exports[`select (isCI = true) > renders disabled options 1`] = `
319446
]
320447
`;
321448
449+
exports[`select (isCI = true) > renders multi-line option labels 1`] = `
450+
[
451+
"<cursor.hide>",
452+
"│
453+
◆ foo
454+
│ ● Option 0
455+
│ with multiple lines
456+
│ ○ Option 1
457+
└
458+
",
459+
"<cursor.backward count=999><cursor.up count=6>",
460+
"<cursor.down count=2>",
461+
"<erase.down>",
462+
"│ ○ Option 0
463+
│ with multiple lines
464+
│ ● Option 1
465+
└
466+
",
467+
"<cursor.backward count=999><cursor.up count=6>",
468+
"<cursor.down count=1>",
469+
"<erase.down>",
470+
"◇ foo
471+
│ Option 1",
472+
"
473+
",
474+
"<cursor.show>",
475+
]
476+
`;
477+
322478
exports[`select (isCI = true) > renders option hints 1`] = `
323479
[
324480
"<cursor.hide>",
@@ -442,6 +598,30 @@ exports[`select (isCI = true) > wraps long cancelled message 1`] = `
442598
]
443599
`;
444600
601+
exports[`select (isCI = true) > wraps long messages 1`] = `
602+
[
603+
"<cursor.hide>",
604+
"│
605+
◆ foo foo foo foo foo foo foo
606+
│ foo foo foo foo foo foo
607+
│ foo foo foo foo foo foo foo
608+
│ ● opt0
609+
│ ○ opt1
610+
└
611+
",
612+
"<cursor.backward count=999><cursor.up count=7>",
613+
"<cursor.down count=1>",
614+
"<erase.down>",
615+
"◇ foo foo foo foo foo foo foo
616+
│ foo foo foo foo foo foo
617+
│ foo foo foo foo foo foo foo
618+
│ opt0",
619+
"
620+
",
621+
"<cursor.show>",
622+
]
623+
`;
624+
445625
exports[`select (isCI = true) > wraps long results 1`] = `
446626
[
447627
"<cursor.hide>",

0 commit comments

Comments
 (0)