Skip to content
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

Listen to custom key events on list item #1208

Closed
Explosion-Scratch opened this issue Apr 8, 2023 · 8 comments
Closed

Listen to custom key events on list item #1208

Explosion-Scratch opened this issue Apr 8, 2023 · 8 comments

Comments

@Explosion-Scratch
Copy link

How would I go about listening for a custom key press while the user is selecting a list item, for example say I prompt the user to select a file like such:

inquirer.prompt([{type: "checkbox", choices: ["file1.txt", "file2.mp4", "file3.md"]}])

Now how do I make it so that when selecting if the user presses "o" on the active file it will open the file (similar to how Press <i> to invert selection works):

image

(`obsidian_notes` is the selected item here, I want to be able to press "o" while selecting it to run some function, such as opening it in finder)
@SBoudrias
Copy link
Owner

Hi, this isn't something we support in the base API. I would recommend you to create a custom prompt to handle this use case.

I'd recommend you to use the new API to do so. Maybe replicating the new select prompt code and looking into useKeypress().

Hope this helps!

@pgibler
Copy link

pgibler commented Apr 20, 2023

@SBoudrias I have a similar question, not sure if the useKeyPress will work for this or not, but do let me know.

I have this as my output list:

? Choose an option:
❯ Run all commands (A)
Run setup commands (S)
Run main command (M)
Quit (Q)

I want it such that the user can either use the arrow keys + Enter to make a selection, or just hit the key corresponding to the option.

Is there a way to achieve this with the new API?

@SBoudrias
Copy link
Owner

@pgibler if you want something out of the box, the expand prompt is your best bet. Otherwise, you could fork the code from the expand prompt and customize it (with the new core API, the prompt is just ~100 lines of code - so cheap to copy-paste in your project or publish your own custom prompt package).

I think there might be an option to expand the default prompt by default (but I wrote that years ago, so I'm not 100% sure anymore 😅)

@pgibler
Copy link

pgibler commented Apr 21, 2023

I've followed your instructions and I almost have it done, but I've run into one bug that I'm wondering if you may remember the code well enough to know a solution. I'm going to try and figure it out myself so if I discover the fix I'll followup to help out anyone else who runs into the same thing.

It occurs when the prompt constructor is picked based on the question.type. There is a key / value pair for my customList in the configured prompts in PromptUI, but the value that comes back - which is supposed to be a constructor - results in an error when used like a constructor.

Here's what I'm seeing for the data that's being set in the fetchAnswer function in prompt.js (in Inquirer.js)

image

And indeed on moving to the next line, the Prompt field is assigned to the value at the customList prompt key.

image

But when this.activePrompt = new Prompt(question, this.rl, this.answers); is executed, I get this error:

These were the parameters passed into it:

image

Would you know what might be causing this? I can make a repo to allow you to reproduce if it would help.

Ultimately, I get this error:

image

Extra information below:

This is the getCustomMenuChoice function I have:

async function getCustomMenuChoice(nonInteractive, setupCommands) {
  const options = [
    {
      name: 'Run main command (M)',
      value: 'm',
    },
    {
      name: 'Quit (Q)',
      value: 'q',
    },
  ];

  const defaultOption = !nonInteractive ? 'c' : (
    nonInteractive && setupCommands.length > 0 ? 'a' : 'm'
  );

  if (setupCommands.length > 0) {
    options.unshift({ name: 'Run setup commands (S)', value: 's' })
    options.unshift({ name: 'Run all commands (A)', value: 'a' });
  }
  if (!nonInteractive) {
    const spliceIndex = options.findIndex(option => option.value === 'm')
    options.splice(spliceIndex, 0, {
      name: 'Copy command to clipboard (C)',
      value: 'c',
    });
  }

  const answers = await inquirer.prompt([
    {
      type: 'customList',
      name: 'choice',
      message: 'Choose an option:',
      choices: options,
      default: defaultOption,
    },
  ]);

  return answers.choice;
}

This is my customListPrompt.mjs

import {
  createPrompt,
  useState,
  useKeypress,
  usePrefix,
  isEnterKey,
  isUpKey,
  isDownKey,
} from '@inquirer/core';
import chalk from 'chalk';

export default createPrompt((config, done) => {
  const { choices, default: defaultKey = 'm' } = config;
  const [status, setStatus] = useState('pending');
  const [index, setIndex] = useState(choices.findIndex((choice) => choice.value === defaultKey));
  const prefix = usePrefix();

  useKeypress((key, rl) => {
    if (isEnterKey(key)) {
      const selectedChoice = choices[index];
      if (selectedChoice) {
        setStatus('done');
        done(selectedChoice.value);
      }
    } else if (isUpKey) {
      setIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : choices.length - 1));
    } else if (isDownKey) {
      setIndex((prevIndex) => (prevIndex < choices.length - 1 ? prevIndex + 1 : 0));
    } else {
      const foundIndex = choices.findIndex((choice) => choice.value.toLowerCase() === key.name.toLowerCase());
      if (foundIndex !== -1) {
        setIndex(foundIndex);
      }
    }
  });

  const message = chalk.bold(config.message);

  if (status === 'done') {
    return `${prefix} ${message} ${chalk.cyan(choices[index].name)}`;
  }

  const renderedChoices = choices
    .map((choice, i) => {
      const line = `  ${choice.name}`;
      if (i === index) {
        return chalk.cyan(`> ${line}`);
      }

      return `  ${line}`;
    })
    .join('\n');

  return [`${prefix} ${message}`, renderedChoices];
});

I appreciate the help with this! I'm really liking this library.

@pgibler
Copy link

pgibler commented Apr 21, 2023

Figured it out myself after going through your code again @SBoudrias

Here's a working implementation of a list prompt controllable by both up/down + Enter & keypress.

quickSelectPrompt.mjs

import {
  createPrompt,
  useState,
  useKeypress,
  usePrefix,
  isEnterKey,
  isUpKey,
  isDownKey,
} from '@inquirer/core';
import chalk from 'chalk';

export default createPrompt((config, done) => {
  const { choices, default: defaultKey = 'm' } = config;
  const [status, setStatus] = useState('pending');
  const [index, setIndex] = useState(choices.findIndex((choice) => choice.value === defaultKey));
  const prefix = usePrefix();

  useKeypress((key, _rl) => {
    if (isEnterKey(key)) {
      const selectedChoice = choices[index];
      if (selectedChoice) {
        setStatus('done');
        done(selectedChoice.value);
      }
    } else if (isUpKey(key)) {
      setIndex(index > 0 ? index - 1 : 0);
    } else if (isDownKey(key)) {
      setIndex(index < choices.length - 1 ? index + 1 : choices.length - 1);
    } else {
      const foundIndex = choices.findIndex((choice) => choice.value.toLowerCase() === key.name.toLowerCase());
      if (foundIndex !== -1) {
        setIndex(foundIndex);
        // This automatically finishes the prompt. Remove this if you don't want that.
        setStatus('done');
        done(choices[foundIndex].value);
      }
    }
  });

  const message = chalk.bold(config.message);

  if (status === 'done') {
    return `${prefix} ${message} ${chalk.cyan(choices[index].name)}`;
  }

  const renderedChoices = choices
    .map((choice, i) => {
      const line = `  ${choice.name}`;
      if (i === index) {
        return chalk.cyan(`> ${line}`);
      }

      return `  ${line}`;
    })
    .join('\n');

  return [`${prefix} ${message}`, renderedChoices];
});

Then to use it

const answer = await quickSelectPrompt({
  message: 'Choose an option:',
  choices: options,
  default: defaultOption,
});

return answer;

How can I best share this in case other's want to use it? I figure someone else may want this functionality too.

@SBoudrias
Copy link
Owner

How can I best share this in case other's want to use it? I figure someone else may want this functionality too.

You could put the code in a Github repo, and release it as a new package to npm 😄

@pgibler
Copy link

pgibler commented Apr 26, 2023

Just got a repo up with the code now. Thanks for all the help getting this going @SBoudrias.

https://github.com/pgibler/inquirer-interactive-list-prompt

@SBoudrias
Copy link
Owner

@pgibler you can add your prompt here https://github.com/SBoudrias/Inquirer.js/blob/master/packages/prompts/README.md#community-prompts if you want to advertise it! I'm planning to switch this README to the repo homepage next weekend (more on that).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants