Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .changeset/calm-feet-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clack/prompts": minor
---

Add an opt-in `instructions` option to `multiselect` and `groupMultiselect`. When enabled, the active prompt shows a keyboard hints footer. Defaults to `false`, so existing prompts are unchanged.
3 changes: 3 additions & 0 deletions examples/changesets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function main() {
packages: () =>
p.groupMultiselect({
message: 'Which packages would you like to include?',
instructions: true,
options: {
'changed packages': [
{ value: '@scope/a' },
Expand All @@ -36,6 +37,7 @@ async function main() {
const packages = results.packages ?? [];
return p.multiselect({
message: `Which packages should have a ${color.red('major')} bump?`,
instructions: true,
options: packages.map((value) => ({ value })),
required: false,
});
Expand All @@ -47,6 +49,7 @@ async function main() {
if (possiblePackages.length === 0) return;
return p.multiselect({
message: `Which packages should have a ${color.yellow('minor')} bump?`,
instructions: true,
options: possiblePackages.map((value) => ({ value })),
required: false,
});
Expand Down
18 changes: 18 additions & 0 deletions packages/prompts/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,21 @@ export interface CommonOptions {
signal?: AbortSignal;
withGuide?: boolean;
}

export const MULTISELECT_INSTRUCTIONS = [
Comment thread
43081j marked this conversation as resolved.
Outdated
`${styleText('dim', '↑/↓')} to navigate`,
`${styleText('dim', 'Space:')} select`,
`${styleText('dim', 'Enter:')} confirm`,
];

export function formatInstructionFooter(
instructions: string[] | null,
hasGuide: boolean
): { text: string; lineCount: number } {
Comment thread
43081j marked this conversation as resolved.
Outdated
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
const footerLines = instructions ? [`${guidePrefix}${instructions.join(' • ')}`] : [];
if (hasGuide) {
footerLines.push(styleText('cyan', S_BAR_END));
}
return { text: footerLines.join('\n'), lineCount: footerLines.length + 1 };
}
19 changes: 14 additions & 5 deletions packages/prompts/src/group-multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { styleText } from 'node:util';
import { GroupMultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
type CommonOptions,
formatInstructionFooter,
MULTISELECT_INSTRUCTIONS,
S_BAR,
S_BAR_END,
S_CHECKBOX_ACTIVE,
Expand Down Expand Up @@ -58,6 +60,12 @@ export interface GroupMultiSelectOptions<Value> extends CommonOptions {
* @default 0
*/
groupSpacing?: number;

/**
* Show keyboard instructions below the option list.
* @default false
*/
instructions?: boolean;
}

/**
Expand Down Expand Up @@ -189,6 +197,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
);
};
const required = opts.required ?? true;
const showInstructions = opts.instructions ?? false;

return new GroupMultiSelectPrompt({
options: opts.options,
Expand Down Expand Up @@ -285,9 +294,11 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
}
default: {
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
const titleLineCount = title.split('\n').length;
const footerLineCount = (hasGuide ? 1 : 0) + 1; // guide line + trailing newline
const { text: footerText, lineCount: footerLineCount } = formatInstructionFooter(
showInstructions ? MULTISELECT_INSTRUCTIONS : null,
Comment thread
43081j marked this conversation as resolved.
Outdated
hasGuide
);
const optionsText = limitOptions({
output: opts.output,
options: this.options,
Expand All @@ -297,9 +308,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
rowPadding: titleLineCount + footerLineCount,
style: styleOption,
}).join(`\n${guidePrefix}`);
return `${title}${guidePrefix}${optionsText}\n${
hasGuide ? styleText('cyan', S_BAR_END) : ''
}\n`;
return `${title}${guidePrefix}${optionsText}\n${footerText}\n`;
}
}
},
Expand Down
16 changes: 13 additions & 3 deletions packages/prompts/src/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { styleText } from 'node:util';
import { MultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
type CommonOptions,
formatInstructionFooter,
MULTISELECT_INSTRUCTIONS,
S_BAR,
S_BAR_END,
S_CHECKBOX_ACTIVE,
Expand All @@ -20,6 +22,11 @@ export interface MultiSelectOptions<Value> extends CommonOptions {
maxItems?: number;
required?: boolean;
cursorAt?: Value;
/**
* Show keyboard instructions below the option list.
* @default false
*/
instructions?: boolean;
}
const computeLabel = (label: string, format: (text: string) => string) => {
return label
Expand Down Expand Up @@ -70,6 +77,7 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`;
};
const required = opts.required ?? true;
const showInstructions = opts.instructions ?? false;

return new MultiSelectPrompt({
options: opts.options,
Expand Down Expand Up @@ -171,9 +179,11 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
}
default: {
const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
const titleLineCount = title.split('\n').length;
const footerLineCount = hasGuide ? 2 : 1; // S_BAR_END + trailing newline
const { text: footerText, lineCount: footerLineCount } = formatInstructionFooter(
showInstructions ? MULTISELECT_INSTRUCTIONS : null,
hasGuide
);
return `${title}${prefix}${limitOptions({
output: opts.output,
options: this.options,
Expand All @@ -182,7 +192,7 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
columnPadding: prefix.length,
rowPadding: titleLineCount + footerLineCount,
style: styleOption,
}).join(`\n${prefix}`)}\n${hasGuide ? styleText('cyan', S_BAR_END) : ''}\n`;
}).join(`\n${prefix}`)}\n${footerText}\n`;
}
}
},
Expand Down
Loading
Loading