Skip to content

Commit 1a73fd0

Browse files
committed
Merge branch 'core-multiline' of https://github.com/Mist3rBru/clack into core-multiline
2 parents dd87e31 + 5d3030b commit 1a73fd0

File tree

2 files changed

+89
-99
lines changed

2 files changed

+89
-99
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@
3030
"volta": {
3131
"node": "20.18.1"
3232
}
33-
}
33+
}

packages/prompts/src/index.ts

Lines changed: 88 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import {
99
SelectKeyPrompt,
1010
SelectPrompt,
1111
State,
12-
TextPrompt,
1312
strLength,
13+
TextPrompt
1414
} from '@clack/core';
1515
import isUnicodeSupported from 'is-unicode-supported';
1616
import color from 'picocolors';
@@ -61,43 +61,6 @@ const symbol = (state: State) => {
6161

6262
const format = Prompt.prototype.format;
6363

64-
interface LimitOptionsParams<TOption> {
65-
options: TOption[];
66-
maxItems: number | undefined;
67-
cursor: number;
68-
style: (option: TOption, active: boolean) => string;
69-
}
70-
71-
const limitOptions = <TOption>(params: LimitOptionsParams<TOption>): string[] => {
72-
const { cursor, options, style } = params;
73-
74-
const paramMaxItems = params.maxItems ?? Infinity;
75-
const outputMaxItems = Math.max(process.stdout.rows - 4, 0);
76-
// We clamp to minimum 5 because anything less doesn't make sense UX wise
77-
const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5));
78-
let slidingWindowLocation = 0;
79-
80-
if (cursor >= slidingWindowLocation + maxItems - 3) {
81-
slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0);
82-
} else if (cursor < slidingWindowLocation + 2) {
83-
slidingWindowLocation = Math.max(cursor - 2, 0);
84-
}
85-
86-
const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0;
87-
const shouldRenderBottomEllipsis =
88-
maxItems < options.length && slidingWindowLocation + maxItems < options.length;
89-
90-
return options
91-
.slice(slidingWindowLocation, slidingWindowLocation + maxItems)
92-
.map((option, i, arr) => {
93-
const isTopLimit = i === 0 && shouldRenderTopEllipsis;
94-
const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis;
95-
return isTopLimit || isBottomLimit
96-
? color.dim('...')
97-
: style(option, i + slidingWindowLocation === cursor);
98-
});
99-
};
100-
10164
interface ThemeParams {
10265
ctx: Omit<Prompt, 'prompt'>;
10366
message: string;
@@ -138,7 +101,10 @@ function applyTheme(data: ThemeParams): string {
138101
style: (line) => color.strikethrough(color.dim(line)),
139102
},
140103
}),
141-
].join('\n');
104+
value ? color.gray(S_BAR) : null,
105+
]
106+
.filter(Boolean)
107+
.join('\n');
142108

143109
case 'error':
144110
return [
@@ -192,6 +158,43 @@ function applyTheme(data: ThemeParams): string {
192158
}
193159
}
194160

161+
interface LimitOptionsParams<TOption> {
162+
options: TOption[];
163+
maxItems: number | undefined;
164+
cursor: number;
165+
style: (option: TOption, active: boolean) => string;
166+
}
167+
168+
const limitOptions = <TOption>(params: LimitOptionsParams<TOption>): string[] => {
169+
const { cursor, options, style } = params;
170+
171+
const paramMaxItems = params.maxItems ?? Infinity;
172+
const outputMaxItems = Math.max(process.stdout.rows - 4, 0);
173+
// We clamp to minimum 5 because anything less doesn't make sense UX wise
174+
const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5));
175+
let slidingWindowLocation = 0;
176+
177+
if (cursor >= slidingWindowLocation + maxItems - 3) {
178+
slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0);
179+
} else if (cursor < slidingWindowLocation + 2) {
180+
slidingWindowLocation = Math.max(cursor - 2, 0);
181+
}
182+
183+
const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0;
184+
const shouldRenderBottomEllipsis =
185+
maxItems < options.length && slidingWindowLocation + maxItems < options.length;
186+
187+
return options
188+
.slice(slidingWindowLocation, slidingWindowLocation + maxItems)
189+
.map((option, i, arr) => {
190+
const isTopLimit = i === 0 && shouldRenderTopEllipsis;
191+
const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis;
192+
return isTopLimit || isBottomLimit
193+
? color.dim('...')
194+
: style(option, i + slidingWindowLocation === cursor);
195+
});
196+
};
197+
195198
export interface TextOptions {
196199
message: string;
197200
placeholder?: string;
@@ -285,7 +288,10 @@ export interface SelectOptions<Value> {
285288
}
286289

287290
export const select = <Value>(opts: SelectOptions<Value>) => {
288-
const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'selected' | 'cancelled') => {
291+
const opt = (
292+
option: Option<Value>,
293+
state: 'inactive' | 'active' | 'selected' | 'cancelled'
294+
): string => {
289295
const label = option.label ?? String(option.value);
290296
switch (state) {
291297
case 'selected':
@@ -311,17 +317,16 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
311317
value = opt(this.options[this.cursor], 'selected');
312318
break;
313319
case 'cancel':
314-
return `${title}${color.gray(S_BAR)} ${opt(
315-
this.options[this.cursor],
316-
'cancelled'
317-
)}\n${color.gray(S_BAR)}`;
320+
value = opt(this.options[this.cursor], 'cancelled');
321+
break;
318322
default: {
319-
return `${title}${color.cyan(S_BAR)} ${limitOptions({
323+
value = limitOptions({
320324
cursor: this.cursor,
321325
options: this.options,
322326
maxItems: opts.maxItems,
323327
style: (item, active) => opt(item, active ? 'active' : 'inactive'),
324-
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
328+
}).join('\n');
329+
break;
325330
}
326331
}
327332
return applyTheme({
@@ -362,16 +367,17 @@ export const selectKey = <Value extends string>(opts: SelectOptions<Value>) => {
362367

363368
switch (this.state) {
364369
case 'submit':
365-
return `${title}${color.gray(S_BAR)} ${opt(
370+
return `${title}\n${color.gray(S_BAR)} ${opt(
366371
this.options.find((opt) => opt.value === this.value)!,
367372
'selected'
368373
)}`;
369374
case 'cancel':
370-
return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray(
371-
S_BAR
372-
)}`;
375+
return `${title}\n${color.gray(S_BAR)} ${opt(
376+
this.options[0],
377+
'cancelled'
378+
)}\n${color.gray(S_BAR)}`;
373379
default:
374-
return `${title}${color.cyan(S_BAR)} ${this.options
380+
return `${title}\n${color.cyan(S_BAR)} ${this.options
375381
.map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive'))
376382
.join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
377383
}
@@ -442,51 +448,50 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
442448
};
443449

444450
switch (this.state) {
445-
case 'submit': {
451+
case 'submit':
446452
value =
447453
this.options
448454
.filter(({ value }) => this.value.includes(value))
449455
.map((option) => opt(option, 'submitted'))
450456
.join(color.dim(', ')) || color.dim('none');
451457
break;
452-
}
453-
case 'cancel': {
458+
case 'cancel':
454459
value =
455460
this.options
456461
.filter(({ value }) => this.value.includes(value))
457462
.map((option) => opt(option, 'cancelled'))
458463
.join(color.dim(', ')) ?? '';
459-
}
460-
case 'error': {
461-
const footer = this.error
462-
.split('\n')
463-
.map((ln, i) =>
464-
i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}`
465-
)
466-
.join('\n');
467-
return (
468-
title +
469-
color.yellow(S_BAR) +
470-
' ' +
471-
limitOptions({
472-
options: this.options,
473-
cursor: this.cursor,
474-
maxItems: opts.maxItems,
475-
style: styleOption,
476-
}).join(`\n${color.yellow(S_BAR)} `) +
477-
'\n' +
478-
footer +
479-
'\n'
464+
break;
465+
case 'error':
466+
error = format(
467+
this.error
468+
.split('\n')
469+
.map((ln, i) => (i === 0 ? color.yellow(ln) : ln))
470+
.join('\n'),
471+
{
472+
firstLine: {
473+
start: color.yellow(S_BAR_END),
474+
},
475+
default: {
476+
start: color.hidden('-'),
477+
},
478+
}
480479
);
481-
}
482-
default: {
483-
return `${title}${color.cyan(S_BAR)} ${limitOptions({
480+
value = limitOptions({
481+
cursor: this.cursor,
482+
maxItems: opts.maxItems,
484483
options: this.options,
484+
style: styleOption,
485+
}).join('\n');
486+
break;
487+
default:
488+
value = limitOptions({
485489
cursor: this.cursor,
486490
maxItems: opts.maxItems,
491+
options: this.options,
487492
style: styleOption,
488-
}).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
489-
}
493+
}).join('\n');
494+
break;
490495
}
491496
return applyTheme({
492497
ctx: this,
@@ -564,21 +569,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
564569
)}`;
565570
},
566571
render() {
567-
const symbol = (state: State) => {
568-
switch (state) {
569-
case 'initial':
570-
case 'active':
571-
return color.cyan(S_STEP_ACTIVE);
572-
case 'cancel':
573-
return color.red(S_STEP_CANCEL);
574-
case 'error':
575-
return color.yellow(S_STEP_ERROR);
576-
case 'submit':
577-
return color.green(S_STEP_SUBMIT);
578-
}
579-
};
580-
581-
let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
572+
let title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}`;
582573

583574
switch (this.state) {
584575
case 'submit': {
@@ -861,9 +852,8 @@ export const spinner = () => {
861852
: code === 1
862853
? color.red(S_STEP_CANCEL)
863854
: color.red(S_STEP_ERROR);
864-
process.stdout.write(cursor.move(-999, 0));
865-
process.stdout.write(erase.down(1));
866-
process.stdout.write(`${step} ${_message}\n`);
855+
_message = formatMessage(step, msg || _message);
856+
process.stdout.write(_message + '\n');
867857
clearHooks();
868858
unblock();
869859
};

0 commit comments

Comments
 (0)