Feature request: Assign keyboard key to select() choices
Closed this issue · 2 comments
Hello,
To speed up using select()
, I would like to see the possibility to assign some keyboard key to each choice.
It could look something like one of these:
<?php
$choice = select('What\'s your favorite color?', [
'blue' => ['Blue', 'b'],
'red' => ['Red', 'r'],
]);
$choice = select('What\'s your favorite color?', [
'blue' => new Choice(label: 'Blue', key: 'b'),
'red' => new Choice(label: 'Red', key: 'r'),
]);
$choice = select('What\'s your favorite color?', [
'blue' => '_B_lue',
'red' => '_R_ed',
]);
$choice = select('What\'s your favorite color?', [
'blue' => '[B]lue',
'red' => '[R]ed',
]);
I think the last two ones are the ones that requires the least modification but may not be the most readable.
As for the implementation, I feel this is mostly updating this listener: https://github.com/laravel/prompts/blob/main/src/SelectPrompt.php#L51
On the display side, it would be nice if we could underline the assigned key.
The constructor should make sure that every key is unique.
Here's a quick and dirty class that extends SelectPrompt
. Note that k, h, j and l keys had to be removed:
<?php
declare(strict_types=1);
namespace App\Console;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Laravel\Prompts\Key;
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\Themes\Default\SelectPromptRenderer;
class SelectQuickPrompt extends SelectPrompt
{
public function __construct(
public string $label,
array|Collection $options,
public int|string|null $default = null,
public int $scroll = 5,
public mixed $validate = null,
public string $hint = '',
public bool|string $required = true,
) {
if ($this->required === false) {
throw new InvalidArgumentException('Argument [required] must be true or a string.');
}
$this->options = $options instanceof Collection ? $options->all() : $options;
// This is new
$keys = [];
$i = 0;
foreach ($this->options as &$option) {
if(1===preg_match('`\[(\w)]`', $option, $matches)) {
$option = str_replace($matches[0], self::underline($matches[1]), $option);
if(array_key_exists(strtolower($matches[1]), $keys)) {
throw new InvalidArgumentException('Key '.strtolower($matches[1]).' is defined more than once');
}
$keys[strtolower($matches[1])] = $i;
}
$i++;
}
// End of new
if ($this->default) {
if (array_is_list($this->options)) {
$this->initializeScrolling(array_search($this->default, $this->options) ?: 0);
} else {
$this->initializeScrolling(array_search($this->default, array_keys($this->options)) ?: 0);
}
$this->scrollToHighlighted(count($this->options));
} else {
$this->initializeScrolling(0);
}
$this->on('key', fn ($key) => match ($key) {
Key::UP, Key::UP_ARROW, Key::LEFT, Key::LEFT_ARROW, Key::SHIFT_TAB, Key::CTRL_P, Key::CTRL_B => $this->highlightPrevious(count($this->options)),
Key::DOWN, Key::DOWN_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::TAB, Key::CTRL_N, Key::CTRL_F => $this->highlightNext(count($this->options)),
Key::oneOf([Key::HOME, Key::CTRL_A], $key) => $this->highlight(0),
Key::oneOf([Key::END, Key::CTRL_E], $key) => $this->highlight(count($this->options) - 1),
// This is new
array_key_exists($key, $keys) ? $key : null => $this->highlight($keys[$key]),
// End of new
Key::ENTER => $this->submit(),
default => null,
});
}
protected function getRenderer(): callable
{
return new SelectPromptRenderer($this);
}
public static function select(): int|string
{
return (new self(...func_get_args()))->prompt();
}
}
This is an interesting idea. I've often thought about adding a Laravel\Prompts\option
function that could be passed to select
/multiselect
/search
/multisearch
to enable various new features like disabling options, adding descriptions/hints, and having more control over the return value. Similar to the objects you can pass to terkelg/prompts and clack/prompts.
One of the biggest challenges is that we need to be able to use Symfony's console components as a fallback for Windows users without WSL and maintain any critical behaviour (e.g. disabled options should still be disabled in some way, which Symfony doesn't handle natively). In this case, the keyboard functionality wouldn't be possible with Symfony (as far as I'm aware), but it's not a critical behaviour, so we'd just need to transform the options before passing them to Symfony.
An alternative would be to add this functionality automatically, i.e., automatically choose the first unique character in each option.
(I'd imagine it would be case insensitive)
We could potentially skip characters with existing behaviour like hjkl
, and we'd probably want to limit it to a-z0-9
.
For consistency, the behaviour would need to at least work on select
and multiselect
, which raises the question of whether the key should just move to the option, or whether it should also select it (or toggle it in the case of multiselect
). It also makes me wonder whether it should work in search
and multisearch
. In that scenario, the functionality would need to be conditional based on whether you're focused on the text input or the options (and it would be cool if the underline only appeared when focused on the options).
I don't have the bandwidth to take this on right now, but I'm open to a PR. I'd personally lean towards the automatic version, which also has the benefit of not impacting the Symfony fallback, as the options array would remain unchanged. I don't think it's essential that the search
and multisearch
get the behaviour as they already have a built-in way to get to an option quickly, and it would be a lot more complex because the options array is constantly changing.