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

Feature request: Assign keyboard key to select() choices #144

Closed
Mika56 opened this issue May 3, 2024 · 2 comments
Closed

Feature request: Assign keyboard key to select() choices #144

Mika56 opened this issue May 3, 2024 · 2 comments
Assignees

Comments

@Mika56
Copy link

Mika56 commented May 3, 2024

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.

@Mika56
Copy link
Author

Mika56 commented May 3, 2024

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();
    }

}

@jessarcher
Copy link
Member

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.

image

(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.

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

No branches or pull requests

3 participants