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
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 keyboard instruction footers to `select`, `multiselect`, and `groupMultiselect` in the active state, matching autocomplete. No option — always shown.
9 changes: 9 additions & 0 deletions packages/prompts/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,12 @@ export interface CommonOptions {
signal?: AbortSignal;
withGuide?: boolean;
}

export function formatInstructionFooter(instructions: string[], hasGuide: boolean): string[] {
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
const footerLines = [`${guidePrefix}${instructions.join(' • ')}`];
if (hasGuide) {
footerLines.push(styleText('cyan', S_BAR_END));
}
return footerLines;
}
11 changes: 6 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,7 @@ import { styleText } from 'node:util';
import { GroupMultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
type CommonOptions,
formatInstructionFooter,
S_BAR,
S_BAR_END,
S_CHECKBOX_ACTIVE,
Expand All @@ -10,6 +11,7 @@ import {
symbol,
} from './common.js';
import { limitOptions } from './limit-options.js';
import { MULTISELECT_INSTRUCTIONS } from './multi-select.js';
import type { Option } from './select.js';

/**
Expand Down Expand Up @@ -285,9 +287,10 @@ 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 footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide);
const footerText = footerLines.join('\n');
const footerLineCount = footerLines.length + 1;
const optionsText = limitOptions({
output: opts.output,
options: this.options,
Expand All @@ -297,9 +300,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
14 changes: 11 additions & 3 deletions packages/prompts/src/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { styleText } from 'node:util';
import { MultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
type CommonOptions,
formatInstructionFooter,
S_BAR,
S_BAR_END,
S_CHECKBOX_ACTIVE,
Expand All @@ -13,6 +14,12 @@ import {
import { limitOptions } from './limit-options.js';
import type { Option } from './select.js';

export const MULTISELECT_INSTRUCTIONS = [
`${styleText('dim', '↑/↓')} to navigate`,
`${styleText('dim', 'Space:')} select`,
`${styleText('dim', 'Enter:')} confirm`,
];

export interface MultiSelectOptions<Value> extends CommonOptions {
message: string;
options: Option<Value>[];
Expand Down Expand Up @@ -171,9 +178,10 @@ 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 footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide);
const footerText = footerLines.join('\n');
const footerLineCount = footerLines.length + 1;
return `${title}${prefix}${limitOptions({
output: opts.output,
options: this.options,
Expand All @@ -182,7 +190,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
15 changes: 10 additions & 5 deletions packages/prompts/src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { styleText } from 'node:util';
import { SelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
import {
type CommonOptions,
formatInstructionFooter,
S_BAR,
S_BAR_END,
S_RADIO_ACTIVE,
S_RADIO_INACTIVE,
symbol,
symbolBar,
} from './common.js';
import { limitOptions } from './limit-options.js';

export const SELECT_INSTRUCTIONS = [
`${styleText('dim', '↑/↓')} to navigate`,
`${styleText('dim', 'Enter:')} confirm`,
];

type Primitive = Readonly<string | boolean | number>;

export type Option<Value> = Value extends Primitive
Expand Down Expand Up @@ -145,10 +150,10 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
}
default: {
const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
const prefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : '';
// 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 (or just trailing newline)
const footerLines = formatInstructionFooter(SELECT_INSTRUCTIONS, hasGuide);
const footerText = footerLines.join('\n');
const footerLineCount = footerLines.length + 1;
return `${title}${prefix}${limitOptions({
output: opts.output,
cursor: this.cursor,
Expand All @@ -158,7 +163,7 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
rowPadding: titleLineCount + footerLineCount,
style: (item, active) =>
opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
}).join(`\n${prefix}`)}\n${prefixEnd}\n`;
}).join(`\n${prefix}`)}\n${footerText}\n`;
}
}
},
Expand Down
Loading
Loading