Inquirer.js icon indicating copy to clipboard operation
Inquirer.js copied to clipboard

custom exit handlers

Open aydn opened this issue 9 years ago • 6 comments

https://github.com/SBoudrias/Inquirer.js/blob/master/lib/ui/baseUI.js#L21

this.rl.on('SIGINT', this.onForceClose); process.on('exit', this.onForceClose);

Please provide an option or function to set exit handlers our own.

aydn avatar Aug 08 '16 21:08 aydn

Why not listen to those events yourself?

SBoudrias avatar Aug 09 '16 01:08 SBoudrias

Those events never triggers in prompt mode. I want to return back to prev. menu or cancel a step with this keys.

'use strict';

process.on('SIGINT', () => console.log('bye!'));

const inquirer = require('inquirer');

inquirer.prompt({
  type: 'input',
  name: 'test',
  message: 'hit CTRL + C to test SIGINT in prompt mode'
});

aydn avatar Aug 09 '16 19:08 aydn

You can pass your own event listener like @SBoudrias suggested. I ended up doing this in my app since I needed synchronous behavior.

let stdin = process.stdin;
stdin.on("data", (key) => {
    if (key == "\u0003") {
        LOG("catching ctrl+c");
    }
});

inquirer.prompt(questions, {input: stdin});

cqcwillard avatar Dec 19 '16 20:12 cqcwillard

I would prefer prompt() to return a rejected promise on Ctrl-C. This is achievable if you're willing to reach in and monkey with a Prompt object's readline directly:

/**
 * By default Inquirer handles Ctrl-C itself by force-quitting the process with
 * no way to clean up. This wrapper around Inquirer throws a Error
 * instead, allowing normal exception handling.
 */
async function safePrompt<T>(question: inquirer.Question<T>): Promise<T> {
	const promptModule = inquirer.createPromptModule()
	const ui = new inquirer.ui.Prompt((promptModule as any).prompts, {})
	const deferred = PromiseUtils.deferred<T>()

	// Remove the force-quit behavior
	const rl = ui.rl
	rl.listeners("SIGINT").forEach(listener => rl.off("SIGINT", listener as any))

	// Insert our listener to reject the promise
	function handleCtrlC() {
		// remove the listener
		rl.off("SIGINT", handleCtrlC)

		// Clean up inquirer
		ui.close()

		// Then reject our promise
		deferred.reject(
			new Error("Aborted due to Ctrl-C during a prompt", ui)
		)
	}
	rl.on("SIGINT", handleCtrlC)

	// Run the UI
	ui.run<T>([question]).then(deferred.resolve, deferred.reject)
	return await deferred.promise
}

(Implementation of PromiseUtils.deferred left as an exercise to the reader)

justjake avatar May 23 '19 22:05 justjake

I would prefer prompt() to return a rejected promise on Ctrl-C. This is achievable if you're willing to reach in and monkey with a Prompt object's readline directly:

/**
 * By default Inquirer handles Ctrl-C itself by force-quitting the process with
 * no way to clean up. This wrapper around Inquirer throws a Error
 * instead, allowing normal exception handling.
 */
async function safePrompt<T>(question: inquirer.Question<T>): Promise<T> {
	const promptModule = inquirer.createPromptModule()
	const ui = new inquirer.ui.Prompt((promptModule as any).prompts, {})
	const deferred = PromiseUtils.deferred<T>()

	// Remove the force-quit behavior
	const rl = ui.rl
	rl.listeners("SIGINT").forEach(listener => rl.off("SIGINT", listener as any))

	// Insert our listener to reject the promise
	function handleCtrlC() {
		// remove the listener
		rl.off("SIGINT", handleCtrlC)

		// Clean up inquirer
		ui.close()

		// Then reject our promise
		deferred.reject(
			new Error("Aborted due to Ctrl-C during a prompt", ui)
		)
	}
	rl.on("SIGINT", handleCtrlC)

	// Run the UI
	ui.run<T>([question]).then(deferred.resolve, deferred.reject)
	return await deferred.promise
}

(Implementation of PromiseUtils.deferred left as an exercise to the reader)

Hey, i'm using inquirer on nodejs, i would like to use this function but in a js file

vorpax avatar Nov 28 '20 17:11 vorpax

i called the safePrompt using @justjake's code,

type Answer = {
  templateName: string;
};

export async function askTemplateName() {
  return safePrompt<Answer>([
    {
      name: 'templateName',
      type: 'list',
      message: 'template name:',
      choices: ['node.js', 'next.js'],
    },
  ]);
}

but i've seen an error like below, i think i definitely passed the name property to the Question object. i don't know why this error occurred.

node_modules/inquirer/lib/prompts/base.js:81
    throw new Error('You must provide a `' + name + '` parameter');
    at InputPrompt.throwParamError (node_modules/inquirer/lib/prompts/base.js:81:11)
    at new Prompt (node_modules/inquirer/lib/prompts/base.js:38:12)
    at new InputPrompt (node_modules/inquirer/lib/prompts/input.js:11:1)
    at PromptUI.fetchAnswer (node_modules/inquirer/lib/ui/prompt.js:106:25)
    at doInnerSub (node_modules/rxjs/src/internal/operators/mergeInternals.ts:71:15)
    at outerNext (node_modules/rxjs/src/internal/operators/mergeInternals.ts:53:58)
    at OperatorSubscriber._this._next (node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts:70:13)
    at OperatorSubscriber.Subscriber.next (node_modules/rxjs/src/internal/Subscriber.ts:75:12)
    at node_modules/rxjs/src/internal/operators/mergeInternals.ts:85:24
    at OperatorSubscriber._this._next (node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts:70:13)
  1. i implemented 'deferred promise' with reference to link,
export default class Deferred<T> {
  public promise: Promise<T>;

  // @ts-ignore
  public resolve: (value: T | PromiseLike<T>) => void;

  // @ts-ignore
  public reject: (reason?: any) => void;

  constructor() {
    this.promise = new Promise<T>((resolve, reject) => {
      this.resolve = resolve;
      this.reject = reject;
    });
  }
}
  1. and implement safePrompt like below.
export async function safePrompt<T>(question: DistinctQuestion<T>): Promise<T> {
  const promptModule = inquirer.createPromptModule();
  const ui = new inquirer.ui.Prompt<T>((promptModule as any).prompts);
  const deferred = new Deferred<T>();

  // Remove the force-quit behavior
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  const { rl } = ui;
  rl.listeners('SIGINT').forEach(listener => rl.off('SIGINT', listener as any));

  // Insert our listener to reject the promise
  function handleCtrlC() {
    // remove the listener
    rl.off('SIGINT', handleCtrlC);

    // Clean up inquirer
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    ui.close();

    // Then reject our promise
    deferred.reject(new Error(`Aborted due to Ctrl-C during a prompt. \n${ui.toString()}`));
  }
  rl.on('SIGINT', handleCtrlC);

  // Run the UI
  ui.run([question]).then(deferred.resolve, deferred.reject);
  return deferred.promise;
}

How can i catch the SIGINT signal?

phaethon5882 avatar Aug 10 '22 15:08 phaethon5882

Closing this ticket as stale. Open new issues if you encounter problem with the new Inquirer API.

The new API expose a documented cancel method to stop a readline. And I think the SIGINT handling is cleaner; though lemme know.

SBoudrias avatar Jun 03 '23 21:06 SBoudrias