SBoudrias / Inquirer.js

A collection of common interactive command line user interfaces.
MIT License
20.28k stars 1.3k forks source link

custom exit handlers #405

Closed aydn closed 1 year ago

aydn commented 8 years ago

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.

SBoudrias commented 8 years ago

Why not listen to those events yourself?

aydn commented 8 years ago

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'
});
cqcwillard commented 7 years ago

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});
justjake commented 5 years ago

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)

vorpax commented 3 years ago

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

phaethon5882 commented 2 years ago

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;
    });
    }
    }
  2. 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?

SBoudrias commented 1 year ago

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.