-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
418 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
|
||
use function Laravel\Prompts\search; | ||
|
||
require __DIR__.'/../vendor/autoload.php'; | ||
|
||
$model = search( | ||
label: 'Which user should receive the email?', | ||
placeholder: 'Search...', | ||
options: function ($value) { | ||
if (strlen($value) === 0) { | ||
return []; | ||
} | ||
|
||
usleep(100 * 1000); | ||
|
||
$count = max(0, 10 - strlen($value)); | ||
|
||
if ($count === 0) { | ||
return []; | ||
} | ||
|
||
return array_map( | ||
fn ($id) => "User $id", | ||
range(0, $count) | ||
); | ||
}, | ||
validate: function ($value) { | ||
if ($value === '0') { | ||
return 'User 0 is not allowed to receive emails.'; | ||
} | ||
} | ||
); | ||
|
||
var_dump($model); | ||
|
||
echo str_repeat(PHP_EOL, 6); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
<?php | ||
|
||
namespace Laravel\Prompts; | ||
|
||
use Closure; | ||
use InvalidArgumentException; | ||
|
||
class SearchPrompt extends Prompt | ||
{ | ||
use Concerns\TypedValue; | ||
|
||
/** | ||
* The index of the highlighted option. | ||
*/ | ||
public int|null $highlighted = null; | ||
|
||
/** | ||
* The cached matches. | ||
* | ||
* @var array<int|string, string>|null | ||
*/ | ||
protected array|null $matches = null; | ||
|
||
/** | ||
* Create a new SuggestPrompt instance. | ||
* | ||
* @param Closure(string): array<int|string, string> $options | ||
*/ | ||
public function __construct( | ||
public string $label, | ||
public Closure $options, | ||
public string $placeholder = '', | ||
public int $scroll = 5, | ||
public ?Closure $validate = null, | ||
) { | ||
$this->trackTypedValue(submit: false); | ||
|
||
$this->on('key', fn ($key) => match ($key) { | ||
Key::UP, Key::SHIFT_TAB => $this->highlightPrevious(), | ||
Key::DOWN, Key::TAB => $this->highlightNext(), | ||
Key::ENTER => $this->highlighted !== null ? $this->submit() : $this->search(), | ||
Key::LEFT, Key::RIGHT => $this->highlighted = null, | ||
default => $this->search(), | ||
}); | ||
} | ||
|
||
protected function search(): void | ||
{ | ||
$this->state = 'searching'; | ||
$this->highlighted = null; | ||
$this->render(); | ||
$this->matches = null; | ||
$this->state = 'active'; | ||
} | ||
|
||
/** | ||
* Get the entered value with a virtual cursor. | ||
*/ | ||
public function valueWithCursor(int $maxWidth): string | ||
{ | ||
if ($this->highlighted !== null) { | ||
return $this->typedValue === '' | ||
? $this->dim($this->truncate($this->placeholder, $maxWidth)) | ||
: $this->truncate($this->typedValue, $maxWidth); | ||
} | ||
|
||
if ($this->typedValue === '') { | ||
return $this->dim($this->addCursor($this->placeholder, 0, $maxWidth)); | ||
} | ||
|
||
return $this->addCursor($this->typedValue, $this->cursorPosition, $maxWidth); | ||
} | ||
|
||
/** | ||
* Get options that match the input. | ||
* | ||
* @return array<string> | ||
*/ | ||
public function matches(): array | ||
{ | ||
if (is_array($this->matches)) { | ||
return $this->matches; | ||
} | ||
|
||
return $this->matches = ($this->options)($this->typedValue); | ||
} | ||
|
||
/** | ||
* Highlight the previous entry, or wrap around to the last entry. | ||
*/ | ||
protected function highlightPrevious(): void | ||
{ | ||
if ($this->matches === []) { | ||
$this->highlighted = null; | ||
} elseif ($this->highlighted === null) { | ||
$this->highlighted = count($this->matches) - 1; | ||
} elseif ($this->highlighted === 0) { | ||
$this->highlighted = null; | ||
} else { | ||
$this->highlighted = $this->highlighted - 1; | ||
} | ||
} | ||
|
||
/** | ||
* Highlight the next entry, or wrap around to the first entry. | ||
*/ | ||
protected function highlightNext(): void | ||
{ | ||
if ($this->matches === []) { | ||
$this->highlighted = null; | ||
} elseif ($this->highlighted === null) { | ||
$this->highlighted = 0; | ||
} else { | ||
$this->highlighted = $this->highlighted === count($this->matches) - 1 ? null : $this->highlighted + 1; | ||
} | ||
} | ||
|
||
public function searchValue(): string | ||
{ | ||
return $this->typedValue; | ||
} | ||
|
||
public function value(): int|string|null | ||
{ | ||
if ($this->matches === null || $this->highlighted === null) { | ||
return null; | ||
} | ||
|
||
return array_is_list($this->matches) | ||
? $this->matches[$this->highlighted] | ||
: array_keys($this->matches)[$this->highlighted]; | ||
} | ||
|
||
/** | ||
* Get the selected label. | ||
*/ | ||
public function label(): string | ||
{ | ||
return $this->matches[array_keys($this->matches)[$this->highlighted]] ?? null; | ||
} | ||
|
||
/** | ||
* Truncate a value with an ellipsis if it exceeds the given length. | ||
*/ | ||
protected function truncate(string $value, int $length): string | ||
{ | ||
if ($length <= 0) { | ||
throw new InvalidArgumentException("Length [{$length}] must be greater than zero."); | ||
} | ||
|
||
return mb_strlen($value) <= $length ? $value : (mb_substr($value, 0, $length - 1).'…'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<?php | ||
|
||
namespace Laravel\Prompts\Themes\Default; | ||
|
||
use Laravel\Prompts\SearchPrompt; | ||
|
||
class SearchPromptRenderer extends Renderer | ||
{ | ||
use Concerns\DrawsBoxes; | ||
use Concerns\DrawsScrollbars; | ||
|
||
/** | ||
* Render the suggest prompt. | ||
*/ | ||
public function __invoke(SearchPrompt $prompt): string | ||
{ | ||
$maxWidth = $prompt->terminal()->cols() - 6; | ||
|
||
return match ($prompt->state) { | ||
'submit' => $this | ||
->box( | ||
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)), | ||
$this->truncate($prompt->label(), $maxWidth), | ||
), | ||
|
||
'cancel' => $this | ||
->box( | ||
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)), | ||
$this->strikethrough($this->dim($this->truncate($prompt->searchValue() ?: $prompt->placeholder, $maxWidth))), | ||
color: 'red', | ||
) | ||
->error('Cancelled'), | ||
|
||
'error' => $this | ||
->box( | ||
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6), | ||
$prompt->valueWithCursor($maxWidth), | ||
$this->renderOptions($prompt), | ||
color: 'yellow', | ||
) | ||
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)), | ||
|
||
'searching' => $this | ||
->box( | ||
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)), | ||
$this->valueWithCursorAndSearchIcon($prompt, $maxWidth), | ||
$this->renderOptions($prompt), | ||
), | ||
|
||
default => $this | ||
->box( | ||
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)), | ||
$prompt->valueWithCursor($maxWidth), | ||
$this->renderOptions($prompt), | ||
) | ||
->spaceForDropdown($prompt) | ||
->newLine(), // Space for errors | ||
}; | ||
} | ||
|
||
/** | ||
* Render the value with the cursor and a search icon. | ||
*/ | ||
protected function valueWithCursorAndSearchIcon(SearchPrompt $prompt, int $maxWidth): string | ||
{ | ||
return preg_replace( | ||
'/\s$/', | ||
$this->cyan('…'), | ||
$this->pad($prompt->valueWithCursor($maxWidth - 1).' ', min($this->longest($prompt->matches(), padding: 2), $maxWidth)) | ||
); | ||
} | ||
|
||
/** | ||
* Render a spacer to prevent jumping when the suggestions are displayed. | ||
*/ | ||
protected function spaceForDropdown(SearchPrompt $prompt): self | ||
{ | ||
if ($prompt->searchValue() !== '') { | ||
return $this; | ||
} | ||
|
||
$this->newLine(max( | ||
0, | ||
min($prompt->scroll, $prompt->terminal()->lines() - 7) - count($prompt->matches()), | ||
)); | ||
|
||
if ($prompt->matches() === []) { | ||
$this->newLine(); | ||
} | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Render the options. | ||
*/ | ||
protected function renderOptions(SearchPrompt $prompt): string | ||
{ | ||
if ($prompt->searchValue() !== '' && empty($prompt->matches())) { | ||
return $this->gray(' '.($prompt->state === 'searching' ? 'Searching...' : 'No results.')); | ||
} | ||
|
||
return $this->scroll( | ||
collect($prompt->matches()) | ||
->values() | ||
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 10)) | ||
->map(fn ($label, $i) => $prompt->highlighted === $i | ||
? "{$this->cyan('›')} {$label} " | ||
: " {$this->dim($label)} " | ||
), | ||
$prompt->highlighted, | ||
min($prompt->scroll, $prompt->terminal()->lines() - 7), | ||
min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6) | ||
)->implode(PHP_EOL); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.