prompts
prompts copied to clipboard
Allow input in autocomplete if not in list
Hi,
I was surprised to see that when using the autocomplete, i'm only able to actually input something from the choices (i know it's what the documentation says, but i still tried 😅).
So i was wondering if it would be a good idea to add a options flag allowing the user input, if it wasn't found in the list. This wouldn't change existing behavior at all, but would allow the use case where you want to use the autocomplete as a suggestive input instead. It could also be made into another prompt type, if that would be easier, extending the autocomplete instead.
Any thoughts?
Hej @LarsVonQualen! I think this could be an option in the current one or maybe a completely new prompt type
@terkelg I thought maybe tab could be used to append the the selected value from the list to the typed value? Like the autocomplete in chrome DevTools :wink:
This behaviour would be activated by an option.
Yeah, that could totally work!
@terkelg I can't use the tab for this purpose cause it would break its current behaviour.
I thought maybe when user input is activated enter would append the selected value and ctrl + enter would submit otherwise if user input isn't activated enter would behave like it currently behave?
On Mac it would be something like cmd + enter instead of ctrl + enter
I see – good point. Sounds good to me!
I just realised I can't do this cause for some reason node does not capture the ctrl, shift or alt key with enter.
I don't really know what we can do now... any idea?
Maybe let's put this on hold for now?
I created a PR in the NodeJS repo that could be used to solve this issue but it still needs to be accepted. With the PR, we will be able to capture Alt + Enter key combo so it will now be possible to do what I had suggested last year.
However, even if the PR is accepted, the new functionalities added by the PR would probably not be supported by the previous NodeJS versions. But anyway since now I know how it works behind the scene, I know how to implement this directly in prompts so we will still support older versions of NodeJS and that also means it doesn't really matter anymore if the PR is accepted or not.
Moreover, while making this PR I dug so deeply into ANSI related source codes, documentations and standards like RXVT, xTerm, ECMA-48 and ISO-6429 that I might possibly be able to implement mouse events and other interesting things I found. Though, I haven't tried the mouse events yet but theoretically I know how, I just need to make it and see if it really works.
@terkelg Let me now what you think.
Refs: https://github.com/nodejs/node/pull/35268
Awesome! Congratulation on the NodeJS contribution! Let's have a hangout soon and plan the next rewrite, I'm super keen to learn more about your findings. High five mate!
Any news on this? If i were to take a stab at this, which solution would be preferable. Option on existing autocomplete type or a new type all together, and what would that be named?
On one hand a simple option should suffice. On the other, it might not be obvious that you're free to type your own name.
I'm not sure if this has been implemented yet, so I went ahead and hacked together my own version. I am posting it so that others can see how I got it working. If someone has a better way to get this functionality, I would love to hear about it. Here is my current solution:
#!/usr/bin/env node
const prompts = require("prompts");
async function main() {
// Print a new line for separation
console.log("");
// List of all autocomplete entries
const entries = ["asdf", "bob", "alice", "AAAA", "javascript", "Hello", "&*$)#FdsaJdsa", "2021"];
// User selects an entry
const { selection } = await prompts(
{
type: "autocomplete",
name: "selection",
message: "Pick an option or press ^c to exit",
// Maps the entries to an array of objects with title, description, and value properties
choices: entries.map((entry) => ({ title: entry, description: `Selects ${entry}`, value: entry })),
// When the user updates the prompt, i.e by pressing a key, we set the
// fallback to an object with the current user input. The fallback object
// is what gets rendered in the gray text, and it is not enough to simply
// set the fallback object. While prompt will return the fallback object
// if no choice is selected, for some reason, it cuts off the last char
// from the title and value, thus we have to set the this.value property
// as well. Also, the onState hook can not be an es6 arrow frunction,
// becuase that will break the binding of the 'this' property, so it must
// be declared like this.
onState: function () {
this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input };
// Check to make sure there are no suggestions so we do not override a suggestion
if (this.suggestions.length === 0) {
this.value = this.input;
}
},
},
{
onCancel: () => {
console.log("Canceled by user, exiting...");
process.exit(1);
},
}
);
console.log(`Selection: ${selection}`);
}
main();
Here it is in action:
Here shows the canceling:
And this is what happens when you only set the fallback object in the onState hook and not update the value:
onState: function () { this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input }; // Check to make sure there are no suggestions so we do not override a suggestion if (this.suggestions.length === 0) { this.value = this.input; } },
In the latest version ,there is no this.suggestions anymore, it has been renamed to filteredOptions.
So use this.filteredOptions.length
But your code also throws TypeError: Cannot read properties of undefined (reading 'filter')
error
What I'm trying to do is make it work with multiselectAutocomplete and by far I already made it work with custom options but it does not saves as selected:
onRender: function () {
this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input }
if (this.filteredOptions.length === 0) {
this.filteredOptions = [
{
title: this.inputValue,
description: undefined,
value: this.inputValue,
selected: this.value.some(({ title }) => title)?.selected,
disabled: undefined
}
]
}
}
Ok I wrote a solution for multiselectAutocomplete propmpt type but it's far from ideal and has a lot of cosmetical bugs, which I will try to fix soon.
- It duplicates last custom result if Return key was pressed after selecting it
- It does not remove un-selected custom result if it was unselected in empty search
- It continues to show selected custom result if you erase last char because it is added to values list
onRender: function () {
// this.fallback = { title: this.input, description: `Selects ${this.input}`, value: this.input }
const options = this.filteredOptions
if (options.length === 0) {
this.filteredOptions = [
{
title: this.inputValue,
description: undefined,
value: this.inputValue,
selected: this.value.some(({ title }) => title)?.selected || undefined,
disabled: undefined,
custom: true
}
]
} else if (options.length === 1 && options[0].custom) {
const customOption = options[0]
if (customOption.selected === true) {
this.value = this.value.concat({ ...customOption, custom: undefined, customAdded: true })
} else if (customOption.selected === false) {
const customOptionIndex = this.value.findIndex(({ title }) => title === customOption.title)
this.value.splice(customOptionIndex, 1)
}
}
// removes custom un-selected options after re-search
this.value = this.value.filter(({ customAdded, selected }) => customAdded ? selected : true)
},
My previous solution still works for me with promps v2.4.2 (which I believe is the latest). ~~The exact code I am using is the same as above.~~ The exact code I am using is:
// User selects the command
const { command } = await prompts(
{
type: "autocomplete",
name: "command",
message: "Enter a command or press ^c to exit",
choices: availableCommands.map((command) => ({ title: command, description: `Executes ${command}`, value: command })),
onState: function () {
// @ts-ignore
this.fallback = { title: this.input, description: `Executes ${this.input}`, value: this.input };
// @ts-ignore
if (this.suggestions.length === 0) {
this.value = this.fallback.value;
}
},
},
{
onCancel: () => Promise.all([console.log("Canceled by user, exiting..."), process.exit()]),
}
);
The this.suggestions
isn't in the types, thats why I have added the @ts-ignore
comment. This is a solution for the autocomplete input type and I have not tested it with the multiselectAutocomplete like you want
@leonitousconforti, using your solution, works perfect with latest version! updated it a bit to get rid of @ts-ignore
onState(this: any) {
this.fallback = { title: this.input, value: this.input }
if (this.suggestions.length === 0) {
this.value = this.fallback.value
}
}
@leonitousconforti, using your solution, works perfect with latest version! updated it a bit to get rid of @ts-ignore
onState(this: any) { this.fallback = { title: this.input, value: this.input } if (this.suggestions.length === 0) { this.value = this.fallback.value } }
Works perfectly