Skip to content

feat: add support for signals to prompts #340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
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/mean-years-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clack/prompts": minor
---

Add support for signals in prompts, allowing them to be aborted.
2 changes: 2 additions & 0 deletions packages/prompts/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
filter: (search: string, opt: Option<Value>) => {
return getFilteredOption(search, opt);
},
signal: opts.signal,
input: opts.input,
output: opts.output,
validate: opts.validate,
Expand Down Expand Up @@ -230,6 +231,7 @@ export const autocompleteMultiselect = <Value>(opts: AutocompleteMultiSelectOpti
return undefined;
},
initialValue: opts.initialValues,
signal: opts.signal,
input: opts.input,
output: opts.output,
render() {
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ export const symbol = (state: State) => {
export interface CommonOptions {
input?: Readable;
output?: Writable;
signal?: AbortSignal;
}
1 change: 1 addition & 0 deletions packages/prompts/src/confirm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const confirm = (opts: ConfirmOptions) => {
return new ConfirmPrompt({
active,
inactive,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValue: opts.initialValue ?? true,
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/group-multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) =>

return new GroupMultiSelectPrompt({
options: opts.options,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValues: opts.initialValues,
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/multi-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const multiselect = <Value>(opts: MultiSelectOptions<Value>) => {

return new MultiSelectPrompt({
options: opts.options,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValues: opts.initialValues,
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const password = (opts: PasswordOptions) => {
return new PasswordPrompt({
validate: opts.validate,
mask: opts.mask ?? S_PASSWORD_MASK,
signal: opts.signal,
input: opts.input,
output: opts.output,
render() {
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/select-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const selectKey = <Value extends string>(opts: SelectOptions<Value>) => {

return new SelectKeyPrompt({
options: opts.options,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValue: opts.initialValue,
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const select = <Value>(opts: SelectOptions<Value>) => {

return new SelectPrompt({
options: opts.options,
signal: opts.signal,
input: opts.input,
output: opts.output,
initialValue: opts.initialValue,
Expand Down
9 changes: 9 additions & 0 deletions packages/prompts/src/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const spinner = ({
errorMessage,
frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'],
delay = unicode ? 80 : 120,
signal,
}: SpinnerOptions = {}): SpinnerResult => {
const isCI = isCIFn();

Expand Down Expand Up @@ -72,6 +73,10 @@ export const spinner = ({
process.on('SIGINT', signalEventHandler);
process.on('SIGTERM', signalEventHandler);
process.on('exit', handleExit);

if (signal) {
signal.addEventListener('abort', signalEventHandler);
}
};

const clearHooks = () => {
Expand All @@ -80,6 +85,10 @@ export const spinner = ({
process.removeListener('SIGINT', signalEventHandler);
process.removeListener('SIGTERM', signalEventHandler);
process.removeListener('exit', handleExit);

if (signal) {
signal.removeEventListener('abort', signalEventHandler);
}
};

const clearPrevMessage = () => {
Expand Down
1 change: 1 addition & 0 deletions packages/prompts/src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const text = (opts: TextOptions) => {
defaultValue: opts.defaultValue,
initialValue: opts.initialValue,
output: opts.output,
signal: opts.signal,
input: opts.input,
render() {
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
Expand Down
40 changes: 40 additions & 0 deletions packages/prompts/test/__snapshots__/autocomplete.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`autocomplete > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo

│ Search: _
│ ● Apple
│ ○ Banana
│ ○ Cherry
│ ○ Grape
│ ○ Orange
│ ↑/↓ to select • Enter: confirm • Type: to search
└",
"
",
"<cursor.show>",
]
`;

exports[`autocomplete > limits displayed options when maxItems is set 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -266,6 +286,26 @@ exports[`autocomplete > supports initialValue 1`] = `
]
`;

exports[`autocompleteMultiselect > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo

│ Search: _
│ ◻ Apple
│ ◻ Banana
│ ◻ Cherry
│ ◻ Grape
│ ◻ Orange
│ ↑/↓ to navigate • Space: select • Enter: confirm • Type: to search
└",
"
",
"<cursor.show>",
]
`;

exports[`autocompleteMultiselect > renders error when empty selection & required is true 1`] = `
[
"<cursor.hide>",
Expand Down
28 changes: 28 additions & 0 deletions packages/prompts/test/__snapshots__/confirm.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`confirm (isCI = false) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ yes?
│ ● Yes / ○ No
└
",
"
",
"<cursor.show>",
]
`;

exports[`confirm (isCI = false) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -149,6 +163,20 @@ exports[`confirm (isCI = false) > right arrow moves to next choice 1`] = `
]
`;

exports[`confirm (isCI = true) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ yes?
│ ● Yes / ○ No
└
",
"
",
"<cursor.show>",
]
`;

exports[`confirm (isCI = true) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`groupMultiselect (isCI = false) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ Select a fruit
│ ◻ group1
│ └ ◻ group1value0
│ ◻ group2
│ └ ◻ group2value0
└
",
"
",
"<cursor.show>",
]
`;

exports[`groupMultiselect (isCI = false) > can deselect an option 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -521,6 +538,23 @@ exports[`groupMultiselect (isCI = false) > values can be non-primitive 1`] = `
]
`;

exports[`groupMultiselect (isCI = true) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ Select a fruit
│ ◻ group1
│ └ ◻ group1value0
│ ◻ group2
│ └ ◻ group2value0
└
",
"
",
"<cursor.show>",
]
`;

exports[`groupMultiselect (isCI = true) > can deselect an option 1`] = `
[
"<cursor.hide>",
Expand Down
30 changes: 30 additions & 0 deletions packages/prompts/test/__snapshots__/multi-select.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`multiselect (isCI = false) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ ◻ opt0
│ ◻ opt1
└
",
"
",
"<cursor.show>",
]
`;

exports[`multiselect (isCI = false) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -595,6 +610,21 @@ exports[`multiselect (isCI = false) > sliding window loops upwards 1`] = `
]
`;

exports[`multiselect (isCI = true) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ ◻ opt0
│ ◻ opt1
└
",
"
",
"<cursor.show>",
]
`;

exports[`multiselect (isCI = true) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down
28 changes: 28 additions & 0 deletions packages/prompts/test/__snapshots__/password.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`password (isCI = false) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ _
└
",
"
",
"<cursor.show>",
]
`;

exports[`password (isCI = false) > renders and clears validation errors 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -140,6 +154,20 @@ exports[`password (isCI = false) > renders message 1`] = `
]
`;

exports[`password (isCI = true) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ _
└
",
"
",
"<cursor.show>",
]
`;

exports[`password (isCI = true) > renders and clears validation errors 1`] = `
[
"<cursor.hide>",
Expand Down
30 changes: 30 additions & 0 deletions packages/prompts/test/__snapshots__/select.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`select (isCI = false) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ ● opt0
│ ○ opt1
└
",
"
",
"<cursor.show>",
]
`;

exports[`select (isCI = false) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down Expand Up @@ -142,6 +157,21 @@ exports[`select (isCI = false) > up arrow selects previous option 1`] = `
]
`;

exports[`select (isCI = true) > can be aborted by a signal 1`] = `
[
"<cursor.hide>",
"│
◆ foo
│ ● opt0
│ ○ opt1
└
",
"
",
"<cursor.show>",
]
`;

exports[`select (isCI = true) > can cancel 1`] = `
[
"<cursor.hide>",
Expand Down
Loading