Skip to content

Commit cc6aab5

Browse files
authored
feat(prompts): add instructions footer for select, multi-select & group-multi-select prompts (#567)
1 parent 2f2b52f commit cc6aab5

10 files changed

Lines changed: 1203 additions & 423 deletions

File tree

‎.changeset/calm-feet-lead.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clack/prompts": minor
3+
---
4+
5+
Add keyboard instruction footers to `select`, `multiselect`, and `groupMultiselect` in the active state, matching autocomplete. No option — always shown.

‎packages/prompts/src/common.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,12 @@ export interface CommonOptions {
7373
signal?: AbortSignal;
7474
withGuide?: boolean;
7575
}
76+
77+
export function formatInstructionFooter(instructions: string[], hasGuide: boolean): string[] {
78+
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
79+
const footerLines = [`${guidePrefix}${instructions.join(' • ')}`];
80+
if (hasGuide) {
81+
footerLines.push(styleText('cyan', S_BAR_END));
82+
}
83+
return footerLines;
84+
}

‎packages/prompts/src/group-multi-select.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { styleText } from 'node:util';
22
import { GroupMultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
33
import {
44
type CommonOptions,
5+
formatInstructionFooter,
56
S_BAR,
67
S_BAR_END,
78
S_CHECKBOX_ACTIVE,
@@ -10,6 +11,7 @@ import {
1011
symbol,
1112
} from './common.js';
1213
import { limitOptions } from './limit-options.js';
14+
import { MULTISELECT_INSTRUCTIONS } from './multi-select.js';
1315
import type { Option } from './select.js';
1416

1517
/**
@@ -285,9 +287,10 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
285287
}
286288
default: {
287289
const guidePrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
288-
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
289290
const titleLineCount = title.split('\n').length;
290-
const footerLineCount = (hasGuide ? 1 : 0) + 1; // guide line + trailing newline
291+
const footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide);
292+
const footerText = footerLines.join('\n');
293+
const footerLineCount = footerLines.length + 1;
291294
const optionsText = limitOptions({
292295
output: opts.output,
293296
options: this.options,
@@ -297,9 +300,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>
297300
rowPadding: titleLineCount + footerLineCount,
298301
style: styleOption,
299302
}).join(`\n${guidePrefix}`);
300-
return `${title}${guidePrefix}${optionsText}\n${
301-
hasGuide ? styleText('cyan', S_BAR_END) : ''
302-
}\n`;
303+
return `${title}${guidePrefix}${optionsText}\n${footerText}\n`;
303304
}
304305
}
305306
},

‎packages/prompts/src/multi-select.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { styleText } from 'node:util';
22
import { MultiSelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
33
import {
44
type CommonOptions,
5+
formatInstructionFooter,
56
S_BAR,
67
S_BAR_END,
78
S_CHECKBOX_ACTIVE,
@@ -13,6 +14,12 @@ import {
1314
import { limitOptions } from './limit-options.js';
1415
import type { Option } from './select.js';
1516

17+
export const MULTISELECT_INSTRUCTIONS = [
18+
`${styleText('dim', '↑/↓')} to navigate`,
19+
`${styleText('dim', 'Space:')} select`,
20+
`${styleText('dim', 'Enter:')} confirm`,
21+
];
22+
1623
export interface MultiSelectOptions<Value> extends CommonOptions {
1724
message: string;
1825
options: Option<Value>[];
@@ -171,9 +178,10 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
171178
}
172179
default: {
173180
const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
174-
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
175181
const titleLineCount = title.split('\n').length;
176-
const footerLineCount = hasGuide ? 2 : 1; // S_BAR_END + trailing newline
182+
const footerLines = formatInstructionFooter(MULTISELECT_INSTRUCTIONS, hasGuide);
183+
const footerText = footerLines.join('\n');
184+
const footerLineCount = footerLines.length + 1;
177185
return `${title}${prefix}${limitOptions({
178186
output: opts.output,
179187
options: this.options,
@@ -182,7 +190,7 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {
182190
columnPadding: prefix.length,
183191
rowPadding: titleLineCount + footerLineCount,
184192
style: styleOption,
185-
}).join(`\n${prefix}`)}\n${hasGuide ? styleText('cyan', S_BAR_END) : ''}\n`;
193+
}).join(`\n${prefix}`)}\n${footerText}\n`;
186194
}
187195
}
188196
},

‎packages/prompts/src/select.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import { styleText } from 'node:util';
22
import { SelectPrompt, settings, wrapTextWithPrefix } from '@clack/core';
33
import {
44
type CommonOptions,
5+
formatInstructionFooter,
56
S_BAR,
6-
S_BAR_END,
77
S_RADIO_ACTIVE,
88
S_RADIO_INACTIVE,
99
symbol,
1010
symbolBar,
1111
} from './common.js';
1212
import { limitOptions } from './limit-options.js';
1313

14+
export const SELECT_INSTRUCTIONS = [
15+
`${styleText('dim', '↑/↓')} to navigate`,
16+
`${styleText('dim', 'Enter:')} confirm`,
17+
];
18+
1419
type Primitive = Readonly<string | boolean | number>;
1520

1621
export type Option<Value> = Value extends Primitive
@@ -145,10 +150,10 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
145150
}
146151
default: {
147152
const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : '';
148-
const prefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : '';
149-
// Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline)
150153
const titleLineCount = title.split('\n').length;
151-
const footerLineCount = hasGuide ? 2 : 1; // S_BAR_END + trailing newline (or just trailing newline)
154+
const footerLines = formatInstructionFooter(SELECT_INSTRUCTIONS, hasGuide);
155+
const footerText = footerLines.join('\n');
156+
const footerLineCount = footerLines.length + 1;
152157
return `${title}${prefix}${limitOptions({
153158
output: opts.output,
154159
cursor: this.cursor,
@@ -158,7 +163,7 @@ export const select = <Value>(opts: SelectOptions<Value>) => {
158163
rowPadding: titleLineCount + footerLineCount,
159164
style: (item, active) =>
160165
opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'),
161-
}).join(`\n${prefix}`)}\n${prefixEnd}\n`;
166+
}).join(`\n${prefix}`)}\n${footerText}\n`;
162167
}
163168
}
164169
},

0 commit comments

Comments
 (0)