Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/giant-dryers-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": patch
"@clack/core": patch
---

Removed all trailing space in prompt output and fixed various padding rendering bugs.
1 change: 1 addition & 0 deletions packages/core/src/prompts/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
if (this.focusedValue) {
this.selectedValues = [this.focusedValue];
}
this.isNavigating = false;
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,16 @@ export default class Prompt<TValue> {

private restoreCursor() {
const lines =
wrap(this._prevFrame, process.stdout.columns, { hard: true }).split('\n').length - 1;
wrap(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split('\n')
.length - 1;
this.output.write(cursor.move(-999, lines * -1));
}

private render() {
const frame = wrap(this._render(this) ?? '', process.stdout.columns, { hard: true });
const frame = wrap(this._render(this) ?? '', process.stdout.columns, {
hard: true,
trim: false,
});
if (frame === this._prevFrame) return;

if (this.state === 'initial') {
Expand Down
23 changes: 14 additions & 9 deletions packages/prompts/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,25 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
case 'submit': {
// Show selected value
const selected = getSelectedOptions(this.selectedValues, options);
const label = selected.length > 0 ? selected.map(getLabel).join(', ') : '';
return `${title}${color.gray(S_BAR)} ${color.dim(label)}`;
const label =
selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : '';
return `${title}${color.gray(S_BAR)}${label}`;
}

case 'cancel': {
return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`;
const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : '';
return `${title}${color.gray(S_BAR)}${userInputText}`;
}

default: {
// Display cursor position - show plain text in navigation mode
const searchText =
this.isNavigating || showPlaceholder
? color.dim(showPlaceholder ? placeholder : userInput)
: this.userInputWithCursor;
let searchText = '';
if (this.isNavigating || showPlaceholder) {
const searchTextValue = showPlaceholder ? placeholder : userInput;
searchText = searchTextValue !== '' ? ` ${color.dim(searchTextValue)}` : '';
} else {
searchText = ` ${this.userInputWithCursor}`;
}

// Show match count if filtered
const matches =
Expand Down Expand Up @@ -164,8 +169,8 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {

// Return the formatted prompt
return [
title,
`${color.cyan(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`,
`${title}${color.cyan(S_BAR)}`,
`${color.cyan(S_BAR)} ${color.dim('Search:')}${searchText}${matches}`,
...noResults,
...validationError,
...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`),
Expand Down
55 changes: 34 additions & 21 deletions packages/prompts/src/group-multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
const isLast = isItem && (next as any).group === true;
const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : '';
const spacingPrefix =
groupSpacing > 0 && !isItem ? `\n${color.cyan(S_BAR)} `.repeat(groupSpacing) : '';
let spacingPrefix = '';
if (groupSpacing > 0 && !isItem) {
const spacingPrefixText = `\n${color.cyan(S_BAR)}`;
spacingPrefix = `${spacingPrefixText.repeat(groupSpacing - 1)}${spacingPrefixText} `;
}

if (state === 'active') {
return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'group-active') {
Expand All @@ -56,16 +59,16 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
}
if (state === 'selected') {
const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : '';
return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'cancelled') {
return `${color.strikethrough(color.dim(label))}`;
}
if (state === 'active-selected') {
return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'submitted') {
Expand Down Expand Up @@ -101,10 +104,12 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>

switch (this.state) {
case 'submit': {
return `${title}${color.gray(S_BAR)} ${this.options
const selectedOptions = this.options
.filter(({ value: optionValue }) => value.includes(optionValue))
.map((option) => opt(option, 'submitted'))
.join(color.dim(', '))}`;
.map((option) => opt(option, 'submitted'));
const optionsText =
selectedOptions.length === 0 ? '' : ` ${selectedOptions.join(color.dim(', '))}`;
return `${title}${color.gray(S_BAR)}${optionsText}`;
}
case 'cancel': {
const label = this.options
Expand Down Expand Up @@ -146,7 +151,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
.join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`;
}
default: {
return `${title}${color.cyan(S_BAR)} ${this.options
const optionsText = this.options
.map((option, i, options) => {
const selected =
value.includes(option.value) ||
Expand All @@ -156,18 +161,26 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
!active &&
typeof option.group === 'string' &&
this.options[this.cursor].value === option.group;
let optionText = '';
if (groupActive) {
return opt(option, selected ? 'group-active-selected' : 'group-active', options);
}
if (active && selected) {
return opt(option, 'active-selected', options);
optionText = opt(
option,
selected ? 'group-active-selected' : 'group-active',
options
);
} else if (active && selected) {
optionText = opt(option, 'active-selected', options);
} else if (selected) {
optionText = opt(option, 'selected', options);
} else {
optionText = opt(option, active ? 'active' : 'inactive', options);
}
if (selected) {
return opt(option, 'selected', options);
}
return opt(option, active ? 'active' : 'inactive', options);
const prefix = i !== 0 && !optionText.startsWith('\n') ? ' ' : '';
return `${prefix}${optionText}`;
})
.join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
.join(`\n${color.cyan(S_BAR)}`);
const optionsPrefix = optionsText.startsWith('\n') ? '' : ' ';
return `${title}${color.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${color.cyan(S_BAR_END)}\n`;
}
}
},
Expand Down
16 changes: 8 additions & 8 deletions packages/prompts/src/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
) => {
const label = option.label ?? String(option.value);
if (state === 'active') {
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'selected') {
return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'cancelled') {
return `${color.strikethrough(color.dim(label))}`;
}
if (state === 'active-selected') {
return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${color.green(S_CHECKBOX_SELECTED)} ${label}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
}
if (state === 'submitted') {
Expand Down Expand Up @@ -98,8 +98,8 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
.filter(({ value: optionValue }) => value.includes(optionValue))
.map((option) => opt(option, 'cancelled'))
.join(color.dim(', '));
return `${title}${color.gray(S_BAR)} ${
label.trim() ? `${label}\n${color.gray(S_BAR)}` : ''
return `${title}${color.gray(S_BAR)}${
label.trim() ? ` ${label}\n${color.gray(S_BAR)}` : ''
}`;
}
case 'error': {
Expand Down
18 changes: 12 additions & 6 deletions packages/prompts/src/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ export const password = (opts: PasswordOptions) => {
const masked = this.masked;

switch (this.state) {
case 'error':
return `${title.trim()}\n${color.yellow(S_BAR)} ${masked}\n${color.yellow(
case 'error': {
const maskedText = masked ? ` ${masked}` : '';
return `${title.trim()}\n${color.yellow(S_BAR)}${maskedText}\n${color.yellow(
S_BAR_END
)} ${color.yellow(this.error)}\n`;
case 'submit':
return `${title}${color.gray(S_BAR)} ${color.dim(masked)}`;
case 'cancel':
return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(masked))}${
}
case 'submit': {
const maskedText = masked ? ` ${color.dim(masked)}` : '';
return `${title}${color.gray(S_BAR)}${maskedText}`;
}
case 'cancel': {
const maskedText = masked ? ` ${color.strikethrough(color.dim(masked))}` : '';
return `${title}${color.gray(S_BAR)}${maskedText}${
masked ? `\n${color.gray(S_BAR)}` : ''
}`;
}
default:
return `${title}${color.cyan(S_BAR)} ${userInput}\n${color.cyan(S_BAR_END)}\n`;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/prompts/src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
case 'selected':
return `${color.dim(label)}`;
case 'active':
return `${color.green(S_RADIO_ACTIVE)} ${label} ${
option.hint ? color.dim(`(${option.hint})`) : ''
return `${color.green(S_RADIO_ACTIVE)} ${label}${
option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''
}`;
case 'cancelled':
return `${color.strikethrough(color.dim(label))}`;
Expand Down
17 changes: 10 additions & 7 deletions packages/prompts/src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@ export const text = (opts: TextOptions) => {
const value = this.value ?? '';

switch (this.state) {
case 'error':
case 'error': {
const errorText = this.error ? ` ${color.yellow(this.error)}` : '';
return `${title.trim()}\n${color.yellow(S_BAR)} ${userInput}\n${color.yellow(
S_BAR_END
)} ${color.yellow(this.error)}\n`;
)}${errorText}\n`;
}
case 'submit': {
return `${title}${color.gray(S_BAR)} ${color.dim(value)}`;
const valueText = value ? ` ${color.dim(value)}` : '';
return `${title}${color.gray(S_BAR)}${valueText}`;
}
case 'cancel': {
const valueText = value ? ` ${color.strikethrough(color.dim(value))}` : '';
return `${title}${color.gray(S_BAR)}${valueText}${value.trim() ? `\n${color.gray(S_BAR)}` : ''}`;
}
case 'cancel':
return `${title}${color.gray(S_BAR)} ${color.strikethrough(
color.dim(value)
)}${value.trim() ? `\n${color.gray(S_BAR)}` : ''}`;
default:
return `${title}${color.cyan(S_BAR)} ${userInput}\n${color.cyan(S_BAR_END)}\n`;
}
Expand Down
Loading