first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

21
vendor/laravel/prompts/LICENSE.md vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

34
vendor/laravel/prompts/README.md vendored Normal file
View File

@ -0,0 +1,34 @@
<p align="center"><img width="386" height="68" src="/art/logo.svg" alt="Laravel Prompts"></p>
<p align="center">
<a href="https://github.com/laravel/prompts/actions"><img src="https://github.com/laravel/prompts/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/prompts"><img src="https://img.shields.io/packagist/dt/laravel/prompts" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/prompts"><img src="https://img.shields.io/packagist/v/laravel/prompts" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/prompts"><img src="https://img.shields.io/packagist/l/laravel/prompts" alt="License"></a>
</p>
## Introduction
Laravel Prompts is a PHP package for adding beautiful and user-friendly forms to your command-line applications, with browser-like features including placeholder text and validation.
Laravel Prompts is perfect for accepting user input in your [Artisan console commands](https://laravel.com/docs/artisan#writing-commands), but it may also be used in any command-line PHP project.
## Official Documentation
Documentation for Laravel Prompts can be found on the [Laravel website](https://laravel.com/docs/prompts).
## Contributing
Thank you for considering contributing to Laravel Prompts! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
Please review [our security policy](https://github.com/laravel/prompts/security/policy) on how to report security vulnerabilities.
## License
Laravel Prompts is open-sourced software licensed under the [MIT license](LICENSE.md).

49
vendor/laravel/prompts/composer.json vendored Normal file
View File

@ -0,0 +1,49 @@
{
"name": "laravel/prompts",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"Laravel\\Prompts\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": "^8.1",
"ext-mbstring": "*",
"illuminate/collections": "^10.0|^11.0",
"symfony/console": "^6.2|^7.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"pestphp/pest": "^2.3",
"mockery/mockery": "^1.5",
"phpstan/phpstan-mockery": "^1.1"
},
"conflict": {
"illuminate/console": ">=10.17.0 <10.25.0",
"laravel/framework": ">=10.17.0 <10.25.0"
},
"suggest": {
"ext-pcntl": "Required for the spinner to be animated."
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"extra": {
"branch-alias": {
"dev-main": "0.1.x-dev"
}
},
"prefer-stable": true,
"minimum-stability": "dev"
}

14
vendor/laravel/prompts/phpunit.xml vendored Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" backupStaticProperties="true">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>

View File

@ -0,0 +1,206 @@
<?php
namespace Laravel\Prompts\Concerns;
trait Colors
{
/**
* Reset all colors and styles.
*/
public function reset(string $text): string
{
return "\e[0m{$text}\e[0m";
}
/**
* Make the text bold.
*/
public function bold(string $text): string
{
return "\e[1m{$text}\e[22m";
}
/**
* Make the text dim.
*/
public function dim(string $text): string
{
return "\e[2m{$text}\e[22m";
}
/**
* Make the text italic.
*/
public function italic(string $text): string
{
return "\e[3m{$text}\e[23m";
}
/**
* Underline the text.
*/
public function underline(string $text): string
{
return "\e[4m{$text}\e[24m";
}
/**
* Invert the text and background colors.
*/
public function inverse(string $text): string
{
return "\e[7m{$text}\e[27m";
}
/**
* Hide the text.
*/
public function hidden(string $text): string
{
return "\e[8m{$text}\e[28m";
}
/**
* Strike through the text.
*/
public function strikethrough(string $text): string
{
return "\e[9m{$text}\e[29m";
}
/**
* Set the text color to black.
*/
public function black(string $text): string
{
return "\e[30m{$text}\e[39m";
}
/**
* Set the text color to red.
*/
public function red(string $text): string
{
return "\e[31m{$text}\e[39m";
}
/**
* Set the text color to green.
*/
public function green(string $text): string
{
return "\e[32m{$text}\e[39m";
}
/**
* Set the text color to yellow.
*/
public function yellow(string $text): string
{
return "\e[33m{$text}\e[39m";
}
/**
* Set the text color to blue.
*/
public function blue(string $text): string
{
return "\e[34m{$text}\e[39m";
}
/**
* Set the text color to magenta.
*/
public function magenta(string $text): string
{
return "\e[35m{$text}\e[39m";
}
/**
* Set the text color to cyan.
*/
public function cyan(string $text): string
{
return "\e[36m{$text}\e[39m";
}
/**
* Set the text color to white.
*/
public function white(string $text): string
{
return "\e[37m{$text}\e[39m";
}
/**
* Set the text background to black.
*/
public function bgBlack(string $text): string
{
return "\e[40m{$text}\e[49m";
}
/**
* Set the text background to red.
*/
public function bgRed(string $text): string
{
return "\e[41m{$text}\e[49m";
}
/**
* Set the text background to green.
*/
public function bgGreen(string $text): string
{
return "\e[42m{$text}\e[49m";
}
/**
* Set the text background to yellow.
*/
public function bgYellow(string $text): string
{
return "\e[43m{$text}\e[49m";
}
/**
* Set the text background to blue.
*/
public function bgBlue(string $text): string
{
return "\e[44m{$text}\e[49m";
}
/**
* Set the text background to magenta.
*/
public function bgMagenta(string $text): string
{
return "\e[45m{$text}\e[49m";
}
/**
* Set the text background to cyan.
*/
public function bgCyan(string $text): string
{
return "\e[46m{$text}\e[49m";
}
/**
* Set the text background to white.
*/
public function bgWhite(string $text): string
{
return "\e[47m{$text}\e[49m";
}
/**
* Set the text color to gray.
*/
public function gray(string $text): string
{
return "\e[90m{$text}\e[39m";
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace Laravel\Prompts\Concerns;
trait Cursor
{
/**
* Indicates if the cursor has been hidden.
*/
protected static bool $cursorHidden = false;
/**
* Hide the cursor.
*/
public function hideCursor(): void
{
static::writeDirectly("\e[?25l");
static::$cursorHidden = true;
}
/**
* Show the cursor.
*/
public function showCursor(): void
{
static::writeDirectly("\e[?25h");
static::$cursorHidden = false;
}
/**
* Restore the cursor if it was hidden.
*/
public function restoreCursor(): void
{
if (static::$cursorHidden) {
$this->showCursor();
}
}
/**
* Move the cursor.
*/
public function moveCursor(int $x, int $y = 0): void
{
$sequence = '';
if ($x < 0) {
$sequence .= "\e[".abs($x).'D'; // Left
} elseif ($x > 0) {
$sequence .= "\e[{$x}C"; // Right
}
if ($y < 0) {
$sequence .= "\e[".abs($y).'A'; // Up
} elseif ($y > 0) {
$sequence .= "\e[{$y}B"; // Down
}
static::writeDirectly($sequence);
}
/**
* Move the cursor to the given column.
*/
public function moveCursorToColumn(int $column): void
{
static::writeDirectly("\e[{$column}G");
}
/**
* Move the cursor up by the given number of lines.
*/
public function moveCursorUp(int $lines): void
{
static::writeDirectly("\e[{$lines}A");
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Laravel\Prompts\Concerns;
trait Erase
{
/**
* Erase the given number of lines downwards from the cursor position.
*/
public function eraseLines(int $count): void
{
$clear = '';
for ($i = 0; $i < $count; $i++) {
$clear .= "\e[2K".($i < $count - 1 ? "\e[{$count}A" : '');
}
if ($count) {
$clear .= "\e[G";
}
static::writeDirectly($clear);
}
/**
* Erase from cursor until end of screen.
*/
public function eraseDown(): void
{
static::writeDirectly("\e[J");
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Laravel\Prompts\Concerns;
use Closure;
trait Events
{
/**
* The registered event listeners.
*
* @var array<string, array<int, Closure>>
*/
protected array $listeners = [];
/**
* Register an event listener.
*/
public function on(string $event, Closure $callback): void
{
$this->listeners[$event][] = $callback;
}
/**
* Emit an event.
*/
public function emit(string $event, mixed ...$data): void
{
foreach ($this->listeners[$event] ?? [] as $listener) {
$listener(...$data);
}
}
/**
* Clean the event listeners.
*/
public function clearListeners(): void
{
$this->listeners = [];
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Laravel\Prompts\Concerns;
use Laravel\Prompts\Output\BufferedConsoleOutput;
use Laravel\Prompts\Terminal;
use PHPUnit\Framework\Assert;
use RuntimeException;
trait FakesInputOutput
{
/**
* Fake the terminal and queue key presses to be simulated.
*
* @param array<string> $keys
*/
public static function fake(array $keys = []): void
{
// Force interactive mode when testing because we will be mocking the terminal.
static::interactive();
$mock = \Mockery::mock(Terminal::class);
$mock->shouldReceive('write')->byDefault();
$mock->shouldReceive('exit')->byDefault();
$mock->shouldReceive('setTty')->byDefault();
$mock->shouldReceive('restoreTty')->byDefault();
$mock->shouldReceive('cols')->byDefault()->andReturn(80);
$mock->shouldReceive('lines')->byDefault()->andReturn(24);
$mock->shouldReceive('initDimensions')->byDefault();
foreach ($keys as $key) {
$mock->shouldReceive('read')->once()->andReturn($key);
}
static::$terminal = $mock;
self::setOutput(new BufferedConsoleOutput());
}
/**
* Assert that the output contains the given string.
*/
public static function assertOutputContains(string $string): void
{
Assert::assertStringContainsString($string, static::content());
}
/**
* Assert that the output doesn't contain the given string.
*/
public static function assertOutputDoesntContain(string $string): void
{
Assert::assertStringNotContainsString($string, static::content());
}
/**
* Assert that the stripped output contains the given string.
*/
public static function assertStrippedOutputContains(string $string): void
{
Assert::assertStringContainsString($string, static::strippedContent());
}
/**
* Assert that the stripped output doesn't contain the given string.
*/
public static function assertStrippedOutputDoesntContain(string $string): void
{
Assert::assertStringNotContainsString($string, static::strippedContent());
}
/**
* Get the buffered console output.
*/
public static function content(): string
{
if (! static::output() instanceof BufferedConsoleOutput) {
throw new RuntimeException('Prompt must be faked before accessing content.');
}
return static::output()->content();
}
/**
* Get the buffered console output, stripped of escape sequences.
*/
public static function strippedContent(): string
{
return preg_replace("/\e\[[0-9;?]*[A-Za-z]/", '', static::content());
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Laravel\Prompts\Concerns;
use Closure;
use RuntimeException;
trait Fallback
{
/**
* Whether to fallback to a custom implementation
*/
protected static bool $shouldFallback = false;
/**
* The fallback implementations.
*
* @var array<class-string, Closure($this): mixed>
*/
protected static array $fallbacks = [];
/**
* Enable the fallback implementation.
*/
public static function fallbackWhen(bool $condition): void
{
static::$shouldFallback = $condition || static::$shouldFallback;
}
/**
* Whether the prompt should fallback to a custom implementation.
*/
public static function shouldFallback(): bool
{
return static::$shouldFallback && isset(static::$fallbacks[static::class]);
}
/**
* Set the fallback implementation.
*
* @param Closure($this): mixed $fallback
*/
public static function fallbackUsing(Closure $fallback): void
{
static::$fallbacks[static::class] = $fallback;
}
/**
* Call the registered fallback implementation.
*/
public function fallback(): mixed
{
$fallback = static::$fallbacks[static::class] ?? null;
if ($fallback === null) {
throw new RuntimeException('No fallback implementation registered for ['.static::class.']');
}
return $fallback($this);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Laravel\Prompts\Concerns;
use Laravel\Prompts\Exceptions\NonInteractiveValidationException;
trait Interactivity
{
/**
* Whether to render the prompt interactively.
*/
protected static bool $interactive;
/**
* Set interactive mode.
*/
public static function interactive(bool $interactive = true): void
{
static::$interactive = $interactive;
}
/**
* Return the default value if it passes validation.
*/
protected function default(): mixed
{
$default = $this->value();
$this->validate($default);
if ($this->state === 'error') {
throw new NonInteractiveValidationException($this->error);
}
return $default;
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace Laravel\Prompts\Concerns;
use Laravel\Prompts\Themes\Contracts\Scrolling as ScrollingRenderer;
trait Scrolling
{
/**
* The number of items to display before scrolling.
*/
public int $scroll;
/**
* The index of the highlighted option.
*/
public ?int $highlighted;
/**
* The index of the first visible option.
*/
public int $firstVisible = 0;
/**
* Initialize scrolling.
*/
protected function initializeScrolling(?int $highlighted = null): void
{
$this->highlighted = $highlighted;
$this->reduceScrollingToFitTerminal();
}
/**
* Reduce the scroll property to fit the terminal height.
*/
protected function reduceScrollingToFitTerminal(): void
{
$reservedLines = ($renderer = $this->getRenderer()) instanceof ScrollingRenderer ? $renderer->reservedLines() : 0;
$this->scroll = max(1, min($this->scroll, $this->terminal()->lines() - $reservedLines));
}
/**
* Highlight the given index.
*/
protected function highlight(?int $index): void
{
$this->highlighted = $index;
if ($this->highlighted === null) {
return;
}
if ($this->highlighted < $this->firstVisible) {
$this->firstVisible = $this->highlighted;
} elseif ($this->highlighted > $this->firstVisible + $this->scroll - 1) {
$this->firstVisible = $this->highlighted - $this->scroll + 1;
}
}
/**
* Highlight the previous entry, or wrap around to the last entry.
*/
protected function highlightPrevious(int $total, bool $allowNull = false): void
{
if ($total === 0) {
return;
}
if ($this->highlighted === null) {
$this->highlight($total - 1);
} elseif ($this->highlighted === 0) {
$this->highlight($allowNull ? null : ($total - 1));
} else {
$this->highlight($this->highlighted - 1);
}
}
/**
* Highlight the next entry, or wrap around to the first entry.
*/
protected function highlightNext(int $total, bool $allowNull = false): void
{
if ($total === 0) {
return;
}
if ($this->highlighted === $total - 1) {
$this->highlight($allowNull ? null : 0);
} else {
$this->highlight(($this->highlighted ?? -1) + 1);
}
}
/**
* Center the highlighted option.
*/
protected function scrollToHighlighted(int $total): void
{
if ($this->highlighted < $this->scroll) {
return;
}
$remaining = $total - $this->highlighted - 1;
$halfScroll = (int) floor($this->scroll / 2);
$endOffset = max(0, $halfScroll - $remaining);
if ($this->scroll % 2 === 0) {
$endOffset--;
}
$this->firstVisible = $this->highlighted - $halfScroll - $endOffset;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Laravel\Prompts\Concerns;
use Laravel\Prompts\Output\BufferedConsoleOutput;
use function Termwind\render;
use function Termwind\renderUsing;
trait Termwind
{
protected function termwind(string $html)
{
renderUsing($output = new BufferedConsoleOutput());
render($html);
return $this->restoreEscapeSequences($output->fetch());
}
protected function restoreEscapeSequences(string $string)
{
return preg_replace('/\[(\d+)m/', "\e[".'\1m', $string);
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace Laravel\Prompts\Concerns;
use InvalidArgumentException;
use Laravel\Prompts\ConfirmPrompt;
use Laravel\Prompts\MultiSearchPrompt;
use Laravel\Prompts\MultiSelectPrompt;
use Laravel\Prompts\Note;
use Laravel\Prompts\PasswordPrompt;
use Laravel\Prompts\PausePrompt;
use Laravel\Prompts\Progress;
use Laravel\Prompts\SearchPrompt;
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\Spinner;
use Laravel\Prompts\SuggestPrompt;
use Laravel\Prompts\Table;
use Laravel\Prompts\TextareaPrompt;
use Laravel\Prompts\TextPrompt;
use Laravel\Prompts\Themes\Default\ConfirmPromptRenderer;
use Laravel\Prompts\Themes\Default\MultiSearchPromptRenderer;
use Laravel\Prompts\Themes\Default\MultiSelectPromptRenderer;
use Laravel\Prompts\Themes\Default\NoteRenderer;
use Laravel\Prompts\Themes\Default\PasswordPromptRenderer;
use Laravel\Prompts\Themes\Default\PausePromptRenderer;
use Laravel\Prompts\Themes\Default\ProgressRenderer;
use Laravel\Prompts\Themes\Default\SearchPromptRenderer;
use Laravel\Prompts\Themes\Default\SelectPromptRenderer;
use Laravel\Prompts\Themes\Default\SpinnerRenderer;
use Laravel\Prompts\Themes\Default\SuggestPromptRenderer;
use Laravel\Prompts\Themes\Default\TableRenderer;
use Laravel\Prompts\Themes\Default\TextareaPromptRenderer;
use Laravel\Prompts\Themes\Default\TextPromptRenderer;
trait Themes
{
/**
* The name of the active theme.
*/
protected static string $theme = 'default';
/**
* The available themes.
*
* @var array<string, array<class-string<\Laravel\Prompts\Prompt>, class-string<object&callable>>>
*/
protected static array $themes = [
'default' => [
TextPrompt::class => TextPromptRenderer::class,
TextareaPrompt::class => TextareaPromptRenderer::class,
PasswordPrompt::class => PasswordPromptRenderer::class,
SelectPrompt::class => SelectPromptRenderer::class,
MultiSelectPrompt::class => MultiSelectPromptRenderer::class,
ConfirmPrompt::class => ConfirmPromptRenderer::class,
PausePrompt::class => PausePromptRenderer::class,
SearchPrompt::class => SearchPromptRenderer::class,
MultiSearchPrompt::class => MultiSearchPromptRenderer::class,
SuggestPrompt::class => SuggestPromptRenderer::class,
Spinner::class => SpinnerRenderer::class,
Note::class => NoteRenderer::class,
Table::class => TableRenderer::class,
Progress::class => ProgressRenderer::class,
],
];
/**
* Get or set the active theme.
*
* @throws \InvalidArgumentException
*/
public static function theme(?string $name = null): string
{
if ($name === null) {
return static::$theme;
}
if (! isset(static::$themes[$name])) {
throw new InvalidArgumentException("Prompt theme [{$name}] not found.");
}
return static::$theme = $name;
}
/**
* Add a new theme.
*
* @param array<class-string<\Laravel\Prompts\Prompt>, class-string<object&callable>> $renderers
*/
public static function addTheme(string $name, array $renderers): void
{
if ($name === 'default') {
throw new InvalidArgumentException('The default theme cannot be overridden.');
}
static::$themes[$name] = $renderers;
}
/**
* Get the renderer for the current prompt.
*/
protected function getRenderer(): callable
{
$class = get_class($this);
return new (static::$themes[static::$theme][$class] ?? static::$themes['default'][$class])($this);
}
/**
* Render the prompt using the active theme.
*/
protected function renderTheme(): string
{
$renderer = $this->getRenderer();
return $renderer($this);
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Laravel\Prompts\Concerns;
use InvalidArgumentException;
trait Truncation
{
/**
* Truncate a value with an ellipsis if it exceeds the given width.
*/
protected function truncate(string $string, int $width): string
{
if ($width <= 0) {
throw new InvalidArgumentException("Width [{$width}] must be greater than zero.");
}
return mb_strwidth($string) <= $width ? $string : (mb_strimwidth($string, 0, $width - 1).'…');
}
/**
* Multi-byte version of wordwrap.
*
* @param non-empty-string $break
*/
protected function mbWordwrap(
string $string,
int $width = 75,
string $break = "\n",
bool $cut_long_words = false
): string {
$lines = explode($break, $string);
$result = [];
foreach ($lines as $originalLine) {
if (mb_strwidth($originalLine) <= $width) {
$result[] = $originalLine;
continue;
}
$words = explode(' ', $originalLine);
$line = null;
$lineWidth = 0;
if ($cut_long_words) {
foreach ($words as $index => $word) {
$characters = mb_str_split($word);
$strings = [];
$str = '';
foreach ($characters as $character) {
$tmp = $str.$character;
if (mb_strwidth($tmp) > $width) {
$strings[] = $str;
$str = $character;
} else {
$str = $tmp;
}
}
if ($str !== '') {
$strings[] = $str;
}
$words[$index] = implode(' ', $strings);
}
$words = explode(' ', implode(' ', $words));
}
foreach ($words as $word) {
$tmp = ($line === null) ? $word : $line.' '.$word;
// Look for zero-width joiner characters (combined emojis)
preg_match('/\p{Cf}/u', $word, $joinerMatches);
$wordWidth = count($joinerMatches) > 0 ? 2 : mb_strwidth($word);
$lineWidth += $wordWidth;
if ($line !== null) {
// Space between words
$lineWidth += 1;
}
if ($lineWidth <= $width) {
$line = $tmp;
} else {
$result[] = $line;
$line = $word;
$lineWidth = $wordWidth;
}
}
if ($line !== '') {
$result[] = $line;
}
$line = null;
}
return implode($break, $result);
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace Laravel\Prompts\Concerns;
use Laravel\Prompts\Key;
trait TypedValue
{
/**
* The value that has been typed.
*/
protected string $typedValue = '';
/**
* The position of the virtual cursor.
*/
protected int $cursorPosition = 0;
/**
* Track the value as the user types.
*/
protected function trackTypedValue(string $default = '', bool $submit = true, ?callable $ignore = null, bool $allowNewLine = false): void
{
$this->typedValue = $default;
if ($this->typedValue) {
$this->cursorPosition = mb_strlen($this->typedValue);
}
$this->on('key', function ($key) use ($submit, $ignore, $allowNewLine) {
if ($key[0] === "\e" || in_array($key, [Key::CTRL_B, Key::CTRL_F, Key::CTRL_A, Key::CTRL_E])) {
if ($ignore !== null && $ignore($key)) {
return;
}
match ($key) {
Key::LEFT, Key::LEFT_ARROW, Key::CTRL_B => $this->cursorPosition = max(0, $this->cursorPosition - 1),
Key::RIGHT, Key::RIGHT_ARROW, Key::CTRL_F => $this->cursorPosition = min(mb_strlen($this->typedValue), $this->cursorPosition + 1),
Key::oneOf([Key::HOME, Key::CTRL_A], $key) => $this->cursorPosition = 0,
Key::oneOf([Key::END, Key::CTRL_E], $key) => $this->cursorPosition = mb_strlen($this->typedValue),
Key::DELETE => $this->typedValue = mb_substr($this->typedValue, 0, $this->cursorPosition).mb_substr($this->typedValue, $this->cursorPosition + 1),
default => null,
};
return;
}
// Keys may be buffered.
foreach (mb_str_split($key) as $key) {
if ($ignore !== null && $ignore($key)) {
return;
}
if ($key === Key::ENTER) {
if ($submit) {
$this->submit();
return;
}
if ($allowNewLine) {
$this->typedValue = mb_substr($this->typedValue, 0, $this->cursorPosition).PHP_EOL.mb_substr($this->typedValue, $this->cursorPosition);
$this->cursorPosition++;
}
} elseif ($key === Key::BACKSPACE || $key === Key::CTRL_H) {
if ($this->cursorPosition === 0) {
return;
}
$this->typedValue = mb_substr($this->typedValue, 0, $this->cursorPosition - 1).mb_substr($this->typedValue, $this->cursorPosition);
$this->cursorPosition--;
} elseif (ord($key) >= 32) {
$this->typedValue = mb_substr($this->typedValue, 0, $this->cursorPosition).$key.mb_substr($this->typedValue, $this->cursorPosition);
$this->cursorPosition++;
}
}
});
}
/**
* Get the value of the prompt.
*/
public function value(): string
{
return $this->typedValue;
}
/**
* Add a virtual cursor to the value and truncate if necessary.
*/
protected function addCursor(string $value, int $cursorPosition, ?int $maxWidth = null): string
{
$before = mb_substr($value, 0, $cursorPosition);
$current = mb_substr($value, $cursorPosition, 1);
$after = mb_substr($value, $cursorPosition + 1);
$cursor = mb_strlen($current) && $current !== PHP_EOL ? $current : ' ';
$spaceBefore = $maxWidth < 0 || $maxWidth === null ? mb_strwidth($before) : $maxWidth - mb_strwidth($cursor) - (mb_strwidth($after) > 0 ? 1 : 0);
[$truncatedBefore, $wasTruncatedBefore] = mb_strwidth($before) > $spaceBefore
? [$this->trimWidthBackwards($before, 0, $spaceBefore - 1), true]
: [$before, false];
$spaceAfter = $maxWidth < 0 || $maxWidth === null ? mb_strwidth($after) : $maxWidth - ($wasTruncatedBefore ? 1 : 0) - mb_strwidth($truncatedBefore) - mb_strwidth($cursor);
[$truncatedAfter, $wasTruncatedAfter] = mb_strwidth($after) > $spaceAfter
? [mb_strimwidth($after, 0, $spaceAfter - 1), true]
: [$after, false];
return ($wasTruncatedBefore ? $this->dim('…') : '')
.$truncatedBefore
.$this->inverse($cursor)
.($current === PHP_EOL ? PHP_EOL : '')
.$truncatedAfter
.($wasTruncatedAfter ? $this->dim('…') : '');
}
/**
* Get a truncated string with the specified width from the end.
*/
private function trimWidthBackwards(string $string, int $start, int $width): string
{
$reversed = implode('', array_reverse(mb_str_split($string, 1)));
$trimmed = mb_strimwidth($reversed, $start, $width);
return implode('', array_reverse(mb_str_split($trimmed, 1)));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Laravel\Prompts;
class ConfirmPrompt extends Prompt
{
/**
* Whether the prompt has been confirmed.
*/
public bool $confirmed;
/**
* Create a new ConfirmPrompt instance.
*/
public function __construct(
public string $label,
public bool $default = true,
public string $yes = 'Yes',
public string $no = 'No',
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->confirmed = $default;
$this->on('key', fn ($key) => match ($key) {
'y' => $this->confirmed = true,
'n' => $this->confirmed = false,
Key::TAB, Key::UP, Key::UP_ARROW, Key::DOWN, Key::DOWN_ARROW, Key::LEFT, Key::LEFT_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::CTRL_P, Key::CTRL_F, Key::CTRL_N, Key::CTRL_B, 'h', 'j', 'k', 'l' => $this->confirmed = ! $this->confirmed,
Key::ENTER => $this->submit(),
default => null,
});
}
/**
* Get the value of the prompt.
*/
public function value(): bool
{
return $this->confirmed;
}
/**
* Get the label of the selected option.
*/
public function label(): string
{
return $this->confirmed ? $this->yes : $this->no;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Laravel\Prompts\Exceptions;
use RuntimeException;
class FormRevertedException extends RuntimeException
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace Laravel\Prompts\Exceptions;
use RuntimeException;
class NonInteractiveValidationException extends RuntimeException
{
//
}

View File

@ -0,0 +1,279 @@
<?php
namespace Laravel\Prompts;
use Closure;
use Illuminate\Support\Collection;
use Laravel\Prompts\Exceptions\FormRevertedException;
class FormBuilder
{
/**
* Each step that should be executed.
*
* @var array<int, \Laravel\Prompts\FormStep>
*/
protected array $steps = [];
/**
* The responses provided by each step.
*
* @var array<mixed>
*/
protected array $responses = [];
/**
* Add a new step.
*/
public function add(Closure $step, ?string $name = null, bool $ignoreWhenReverting = false): self
{
$this->steps[] = new FormStep($step, true, $name, $ignoreWhenReverting);
return $this;
}
/**
* Run all of the given steps.
*
* @return array<mixed>
*/
public function submit(): array
{
$index = 0;
$wasReverted = false;
while ($index < count($this->steps)) {
$step = $this->steps[$index];
if ($wasReverted && $index > 0 && $step->shouldIgnoreWhenReverting($this->responses)) {
$index--;
continue;
}
$wasReverted = false;
$index > 0
? Prompt::revertUsing(function () use (&$wasReverted) {
$wasReverted = true;
}) : Prompt::preventReverting();
try {
$this->responses[$step->name ?? $index] = $step->run(
$this->responses,
$this->responses[$step->name ?? $index] ?? null,
);
} catch (FormRevertedException) {
$wasReverted = true;
}
$wasReverted ? $index-- : $index++;
}
Prompt::preventReverting();
return $this->responses;
}
/**
* Prompt the user for text input.
*/
public function text(string $label, string $placeholder = '', string $default = '', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self
{
return $this->runPrompt(text(...), get_defined_vars());
}
/**
* Prompt the user for multiline text input.
*/
public function textarea(string $label, string $placeholder = '', string $default = '', bool|string $required = false, ?Closure $validate = null, string $hint = '', int $rows = 5, ?string $name = null): self
{
return $this->runPrompt(textarea(...), get_defined_vars());
}
/**
* Prompt the user for input, hiding the value.
*/
public function password(string $label, string $placeholder = '', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self
{
return $this->runPrompt(password(...), get_defined_vars());
}
/**
* Prompt the user to select an option.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param true|string $required
*/
public function select(string $label, array|Collection $options, int|string|null $default = null, int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true, ?string $name = null): self
{
return $this->runPrompt(select(...), get_defined_vars());
}
/**
* Prompt the user to select multiple options.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param array<int|string>|Collection<int, int|string> $default
*/
public function multiselect(string $label, array|Collection $options, array|Collection $default = [], int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.', ?string $name = null): self
{
return $this->runPrompt(multiselect(...), get_defined_vars());
}
/**
* Prompt the user to confirm an action.
*/
public function confirm(string $label, bool $default = true, string $yes = 'Yes', string $no = 'No', bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self
{
return $this->runPrompt(confirm(...), get_defined_vars());
}
/**
* Prompt the user to continue or cancel after pausing.
*/
public function pause(string $message = 'Press enter to continue...', ?string $name = null): self
{
return $this->runPrompt(pause(...), get_defined_vars());
}
/**
* Prompt the user for text input with auto-completion.
*
* @param array<string>|Collection<int, string>|Closure(string): array<string> $options
*/
public function suggest(string $label, array|Collection|Closure $options, string $placeholder = '', string $default = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = '', ?string $name = null): self
{
return $this->runPrompt(suggest(...), get_defined_vars());
}
/**
* Allow the user to search for an option.
*
* @param Closure(string): array<int|string, string> $options
* @param true|string $required
*/
public function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true, ?string $name = null): self
{
return $this->runPrompt(search(...), get_defined_vars());
}
/**
* Allow the user to search for multiple option.
*
* @param Closure(string): array<int|string, string> $options
*/
public function multisearch(string $label, Closure $options, string $placeholder = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.', ?string $name = null): self
{
return $this->runPrompt(multisearch(...), get_defined_vars());
}
/**
* Render a spinner while the given callback is executing.
*
* @param \Closure(): mixed $callback
*/
public function spin(Closure $callback, string $message = '', ?string $name = null): self
{
return $this->runPrompt(spin(...), get_defined_vars(), true);
}
/**
* Display a note.
*/
public function note(string $message, ?string $type = null, ?string $name = null): self
{
return $this->runPrompt(note(...), get_defined_vars(), true);
}
/**
* Display an error.
*/
public function error(string $message, ?string $name = null): self
{
return $this->runPrompt(error(...), get_defined_vars(), true);
}
/**
* Display a warning.
*/
public function warning(string $message, ?string $name = null): self
{
return $this->runPrompt(warning(...), get_defined_vars(), true);
}
/**
* Display an alert.
*/
public function alert(string $message, ?string $name = null): self
{
return $this->runPrompt(alert(...), get_defined_vars(), true);
}
/**
* Display an informational message.
*/
public function info(string $message, ?string $name = null): self
{
return $this->runPrompt(info(...), get_defined_vars(), true);
}
/**
* Display an introduction.
*/
public function intro(string $message, ?string $name = null): self
{
return $this->runPrompt(intro(...), get_defined_vars(), true);
}
/**
* Display a closing message.
*/
public function outro(string $message, ?string $name = null): self
{
return $this->runPrompt(outro(...), get_defined_vars(), true);
}
/**
* Display a table.
*
* @param array<int, string|array<int, string>>|Collection<int, string|array<int, string>> $headers
* @param array<int, array<int, string>>|Collection<int, array<int, string>> $rows
*/
public function table(array|Collection $headers = [], array|Collection|null $rows = null, ?string $name = null): self
{
return $this->runPrompt(table(...), get_defined_vars(), true);
}
/**
* Display a progress bar.
*
* @template TSteps of iterable<mixed>|int
* @template TReturn
*
* @param TSteps $steps
* @param ?Closure((TSteps is int ? int : value-of<TSteps>), Progress<TSteps>): TReturn $callback
*/
public function progress(string $label, iterable|int $steps, ?Closure $callback = null, string $hint = '', ?string $name = null): self
{
return $this->runPrompt(progress(...), get_defined_vars(), true);
}
/**
* Execute the given prompt passing the given arguments.
*
* @param array<mixed> $arguments
*/
protected function runPrompt(callable $prompt, array $arguments, bool $ignoreWhenReverting = false): self
{
return $this->add(function (array $responses, mixed $previousResponse) use ($prompt, $arguments) {
unset($arguments['name']);
if (array_key_exists('default', $arguments) && $previousResponse !== null) {
$arguments['default'] = $previousResponse;
}
return $prompt(...$arguments);
}, name: $arguments['name'], ignoreWhenReverting: $ignoreWhenReverting);
}
}

59
vendor/laravel/prompts/src/FormStep.php vendored Normal file
View File

@ -0,0 +1,59 @@
<?php
namespace Laravel\Prompts;
use Closure;
class FormStep
{
protected readonly Closure $condition;
public function __construct(
protected readonly Closure $step,
bool|Closure $condition,
public readonly ?string $name,
protected readonly bool $ignoreWhenReverting,
) {
$this->condition = is_bool($condition)
? fn () => $condition
: $condition;
}
/**
* Execute this step.
*
* @param array<mixed> $responses
*/
public function run(array $responses, mixed $previousResponse): mixed
{
if (! $this->shouldRun($responses)) {
return null;
}
return ($this->step)($responses, $previousResponse);
}
/**
* Whether the step should run based on the given condition.
*
* @param array<mixed> $responses
*/
protected function shouldRun(array $responses): bool
{
return ($this->condition)($responses);
}
/**
* Whether this step should be skipped over when a subsequent step is reverted.
*
* @param array<mixed> $responses
*/
public function shouldIgnoreWhenReverting(array $responses): bool
{
if (! $this->shouldRun($responses)) {
return true;
}
return $this->ignoreWhenReverting;
}
}

98
vendor/laravel/prompts/src/Key.php vendored Normal file
View File

@ -0,0 +1,98 @@
<?php
namespace Laravel\Prompts;
class Key
{
const UP = "\e[A";
const DOWN = "\e[B";
const RIGHT = "\e[C";
const LEFT = "\e[D";
const UP_ARROW = "\eOA";
const DOWN_ARROW = "\eOB";
const RIGHT_ARROW = "\eOC";
const LEFT_ARROW = "\eOD";
const DELETE = "\e[3~";
const BACKSPACE = "\177";
const ENTER = "\n";
const SPACE = ' ';
const TAB = "\t";
const SHIFT_TAB = "\e[Z";
const HOME = ["\e[1~", "\eOH", "\e[H", "\e[7~"];
const END = ["\e[4~", "\eOF", "\e[F", "\e[8~"];
/**
* Cancel/SIGINT
*/
const CTRL_C = "\x03";
/**
* Previous/Up
*/
const CTRL_P = "\x10";
/**
* Next/Down
*/
const CTRL_N = "\x0E";
/**
* Forward/Right
*/
const CTRL_F = "\x06";
/**
* Back/Left
*/
const CTRL_B = "\x02";
/**
* Backspace
*/
const CTRL_H = "\x08";
/**
* Home
*/
const CTRL_A = "\x01";
/**
* EOF
*/
const CTRL_D = "\x04";
/**
* End
*/
const CTRL_E = "\x05";
/**
* Negative affirmation
*/
const CTRL_U = "\x15";
/**
* Checks for the constant values for the given match and returns the match
*
* @param array<string|array<string>> $keys
*/
public static function oneOf(array $keys, string $match): ?string
{
return collect($keys)->flatten()->contains($match) ? $match : null;
}
}

View File

@ -0,0 +1,190 @@
<?php
namespace Laravel\Prompts;
use Closure;
class MultiSearchPrompt extends Prompt
{
use Concerns\Scrolling;
use Concerns\Truncation;
use Concerns\TypedValue;
/**
* The cached matches.
*
* @var array<int|string, string>|null
*/
protected ?array $matches = null;
/**
* Whether the matches are initially a list.
*/
protected bool $isList;
/**
* The selected values.
*
* @var array<int|string, string>
*/
public array $values = [];
/**
* Create a new MultiSearchPrompt 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 bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue(submit: false, ignore: fn ($key) => Key::oneOf([Key::SPACE, Key::HOME, Key::END, Key::CTRL_A, Key::CTRL_E], $key) && $this->highlighted !== null);
$this->initializeScrolling(null);
$this->on('key', fn ($key) => match ($key) {
Key::UP, Key::UP_ARROW, Key::SHIFT_TAB => $this->highlightPrevious(count($this->matches), true),
Key::DOWN, Key::DOWN_ARROW, Key::TAB => $this->highlightNext(count($this->matches), true),
Key::oneOf([Key::HOME, Key::CTRL_A], $key) => $this->highlighted !== null ? $this->highlight(0) : null,
Key::oneOf([Key::END, Key::CTRL_E], $key) => $this->highlighted !== null ? $this->highlight(count($this->matches()) - 1) : null,
Key::SPACE => $this->highlighted !== null ? $this->toggleHighlighted() : null,
Key::ENTER => $this->submit(),
Key::LEFT, Key::LEFT_ARROW, Key::RIGHT, Key::RIGHT_ARROW => $this->highlighted = null,
default => $this->search(),
});
}
/**
* Perform the search.
*/
protected function search(): void
{
$this->state = 'searching';
$this->highlighted = null;
$this->render();
$this->matches = null;
$this->firstVisible = 0;
$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;
}
$matches = ($this->options)($this->typedValue);
if (! isset($this->isList) && count($matches) > 0) {
// This needs to be captured the first time we receive matches so
// we know what we're dealing with later if matches is empty.
$this->isList = array_is_list($matches);
}
if (! isset($this->isList)) {
return $this->matches = [];
}
if (strlen($this->typedValue) > 0) {
return $this->matches = $matches;
}
return $this->matches = $this->isList
? [...array_diff(array_values($this->values), $matches), ...$matches]
: array_diff($this->values, $matches) + $matches;
}
/**
* The currently visible matches
*
* @return array<string>
*/
public function visible(): array
{
return array_slice($this->matches(), $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Toggle the highlighted entry.
*/
protected function toggleHighlighted(): void
{
if ($this->isList()) {
$label = $this->matches[$this->highlighted];
$key = $label;
} else {
$key = array_keys($this->matches)[$this->highlighted];
$label = $this->matches[$key];
}
if (array_key_exists($key, $this->values)) {
unset($this->values[$key]);
} else {
$this->values[$key] = $label;
}
}
/**
* Get the current search query.
*/
public function searchValue(): string
{
return $this->typedValue;
}
/**
* Get the selected value.
*
* @return array<int|string>
*/
public function value(): array
{
return array_keys($this->values);
}
/**
* Get the selected labels.
*
* @return array<string>
*/
public function labels(): array
{
return array_values($this->values);
}
/**
* Whether the matches are initially a list.
*/
public function isList(): bool
{
return $this->isList;
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace Laravel\Prompts;
use Illuminate\Support\Collection;
class MultiSelectPrompt extends Prompt
{
use Concerns\Scrolling;
/**
* The options for the multi-select prompt.
*
* @var array<int|string, string>
*/
public array $options;
/**
* The default values the multi-select prompt.
*
* @var array<int|string>
*/
public array $default;
/**
* The selected values.
*
* @var array<int|string>
*/
protected array $values = [];
/**
* Create a new MultiSelectPrompt instance.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param array<int|string>|Collection<int, int|string> $default
*/
public function __construct(
public string $label,
array|Collection $options,
array|Collection $default = [],
public int $scroll = 5,
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->options = $options instanceof Collection ? $options->all() : $options;
$this->default = $default instanceof Collection ? $default->all() : $default;
$this->values = $this->default;
$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, 'k', 'h' => $this->highlightPrevious(count($this->options)),
Key::DOWN, Key::DOWN_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::TAB, Key::CTRL_N, Key::CTRL_F, 'j', 'l' => $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),
Key::SPACE => $this->toggleHighlighted(),
Key::ENTER => $this->submit(),
default => null,
});
}
/**
* Get the selected values.
*
* @return array<int|string>
*/
public function value(): array
{
return array_values($this->values);
}
/**
* Get the selected labels.
*
* @return array<string>
*/
public function labels(): array
{
if (array_is_list($this->options)) {
return array_map(fn ($value) => (string) $value, $this->values);
}
return array_values(array_intersect_key($this->options, array_flip($this->values)));
}
/**
* The currently visible options.
*
* @return array<int|string, string>
*/
public function visible(): array
{
return array_slice($this->options, $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Check whether the value is currently highlighted.
*/
public function isHighlighted(string $value): bool
{
if (array_is_list($this->options)) {
return $this->options[$this->highlighted] === $value;
}
return array_keys($this->options)[$this->highlighted] === $value;
}
/**
* Check whether the value is currently selected.
*/
public function isSelected(string $value): bool
{
return in_array($value, $this->values);
}
/**
* Toggle the highlighted entry.
*/
protected function toggleHighlighted(): void
{
$value = array_is_list($this->options)
? $this->options[$this->highlighted]
: array_keys($this->options)[$this->highlighted];
if (in_array($value, $this->values)) {
$this->values = array_filter($this->values, fn ($v) => $v !== $value);
} else {
$this->values[] = $value;
}
}
}

44
vendor/laravel/prompts/src/Note.php vendored Normal file
View File

@ -0,0 +1,44 @@
<?php
namespace Laravel\Prompts;
class Note extends Prompt
{
/**
* Create a new Note instance.
*/
public function __construct(public string $message, public ?string $type = null)
{
//
}
/**
* Display the note.
*/
public function display(): void
{
$this->prompt();
}
/**
* Display the note.
*/
public function prompt(): bool
{
$this->capturePreviousNewLines();
$this->state = 'submit';
static::output()->write($this->renderTheme());
return true;
}
/**
* Get the value of the prompt.
*/
public function value(): bool
{
return true;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Laravel\Prompts\Output;
class BufferedConsoleOutput extends ConsoleOutput
{
/**
* The output buffer.
*/
protected string $buffer = '';
/**
* Empties the buffer and returns its content.
*/
public function fetch(): string
{
$content = $this->buffer;
$this->buffer = '';
return $content;
}
/**
* Return the content of the buffer.
*/
public function content(): string
{
return $this->buffer;
}
/**
* Write to the output buffer.
*/
protected function doWrite(string $message, bool $newline): void
{
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
}
}
/**
* Write output directly, bypassing newline capture.
*/
public function writeDirectly(string $message): void
{
$this->doWrite($message, false);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Laravel\Prompts\Output;
use Symfony\Component\Console\Output\ConsoleOutput as SymfonyConsoleOutput;
class ConsoleOutput extends SymfonyConsoleOutput
{
/**
* How many new lines were written by the last output.
*/
protected int $newLinesWritten = 1;
/**
* How many new lines were written by the last output.
*/
public function newLinesWritten(): int
{
return $this->newLinesWritten;
}
/**
* Write the output and capture the number of trailing new lines.
*/
protected function doWrite(string $message, bool $newline): void
{
parent::doWrite($message, $newline);
if ($newline) {
$message .= \PHP_EOL;
}
$trailingNewLines = strlen($message) - strlen(rtrim($message, \PHP_EOL));
if (trim($message) === '') {
$this->newLinesWritten += $trailingNewLines;
} else {
$this->newLinesWritten = $trailingNewLines;
}
}
/**
* Write output directly, bypassing newline capture.
*/
public function writeDirectly(string $message): void
{
parent::doWrite($message, false);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Laravel\Prompts;
class PasswordPrompt extends Prompt
{
use Concerns\TypedValue;
/**
* Create a new PasswordPrompt instance.
*/
public function __construct(
public string $label,
public string $placeholder = '',
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue();
}
/**
* Get a masked version of the entered value.
*/
public function masked(): string
{
return str_repeat('•', mb_strlen($this->value()));
}
/**
* Get the masked value with a virtual cursor.
*/
public function maskedWithCursor(int $maxWidth): string
{
if ($this->value() === '') {
return $this->dim($this->addCursor($this->placeholder, 0, $maxWidth));
}
return $this->addCursor($this->masked(), $this->cursorPosition, $maxWidth);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Laravel\Prompts;
class PausePrompt extends Prompt
{
/**
* Create a new PausePrompt instance.
*/
public function __construct(public string $message = 'Press enter to continue...')
{
$this->required = false;
$this->validate = null;
$this->on('key', fn ($key) => match ($key) {
Key::ENTER => $this->submit(),
default => null,
});
}
/**
* Get the value of the prompt.
*/
public function value(): bool
{
return static::$interactive;
}
}

205
vendor/laravel/prompts/src/Progress.php vendored Normal file
View File

@ -0,0 +1,205 @@
<?php
namespace Laravel\Prompts;
use Closure;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
/**
* @template TSteps of iterable<mixed>|int
*/
class Progress extends Prompt
{
/**
* The current progress bar item count.
*/
public int $progress = 0;
/**
* The total number of steps.
*/
public int $total = 0;
/**
* The original value of pcntl_async_signals
*/
protected bool $originalAsync;
/**
* Create a new ProgressBar instance.
*
* @param TSteps $steps
*/
public function __construct(public string $label, public iterable|int $steps, public string $hint = '')
{
$this->total = match (true) { // @phpstan-ignore assign.propertyType
is_int($this->steps) => $this->steps,
is_countable($this->steps) => count($this->steps),
is_iterable($this->steps) => iterator_count($this->steps),
default => throw new InvalidArgumentException('Unable to count steps.'),
};
if ($this->total === 0) {
throw new InvalidArgumentException('Progress bar must have at least one item.');
}
}
/**
* Map over the steps while rendering the progress bar.
*
* @template TReturn
*
* @param Closure((TSteps is int ? int : value-of<TSteps>), $this): TReturn $callback
* @return array<TReturn>
*/
public function map(Closure $callback): array
{
$this->start();
$result = [];
try {
if (is_int($this->steps)) {
for ($i = 0; $i < $this->steps; $i++) {
$result[] = $callback($i, $this);
$this->advance();
}
} else {
foreach ($this->steps as $step) {
$result[] = $callback($step, $this);
$this->advance();
}
}
} catch (Throwable $e) {
$this->state = 'error';
$this->render();
$this->restoreCursor();
$this->resetSignals();
throw $e;
}
if ($this->hint !== '') {
// Just pause for one moment to show the final hint
// so it doesn't look like it was skipped
usleep(250_000);
}
$this->finish();
return $result;
}
/**
* Start the progress bar.
*/
public function start(): void
{
$this->capturePreviousNewLines();
if (function_exists('pcntl_signal')) {
$this->originalAsync = pcntl_async_signals(true);
pcntl_signal(SIGINT, function () {
$this->state = 'cancel';
$this->render();
exit();
});
}
$this->state = 'active';
$this->hideCursor();
$this->render();
}
/**
* Advance the progress bar.
*/
public function advance(int $step = 1): void
{
$this->progress += $step;
if ($this->progress > $this->total) {
$this->progress = $this->total;
}
$this->render();
}
/**
* Finish the progress bar.
*/
public function finish(): void
{
$this->state = 'submit';
$this->render();
$this->restoreCursor();
$this->resetSignals();
}
/**
* Update the label.
*/
public function label(string $label): static
{
$this->label = $label;
return $this;
}
/**
* Update the hint.
*/
public function hint(string $hint): static
{
$this->hint = $hint;
return $this;
}
/**
* Get the completion percentage.
*/
public function percentage(): int|float
{
return $this->progress / $this->total;
}
/**
* Disable prompting for input.
*
* @throws \RuntimeException
*/
public function prompt(): never
{
throw new RuntimeException('Progress Bar cannot be prompted.');
}
/**
* Get the value of the prompt.
*/
public function value(): bool
{
return true;
}
/**
* Reset the signal handling.
*/
protected function resetSignals(): void
{
if (isset($this->originalAsync)) {
pcntl_async_signals($this->originalAsync);
pcntl_signal(SIGINT, SIG_DFL);
}
}
/**
* Restore the cursor.
*/
public function __destruct()
{
$this->restoreCursor();
}
}

392
vendor/laravel/prompts/src/Prompt.php vendored Normal file
View File

@ -0,0 +1,392 @@
<?php
namespace Laravel\Prompts;
use Closure;
use Laravel\Prompts\Exceptions\FormRevertedException;
use Laravel\Prompts\Output\ConsoleOutput;
use RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
abstract class Prompt
{
use Concerns\Colors;
use Concerns\Cursor;
use Concerns\Erase;
use Concerns\Events;
use Concerns\FakesInputOutput;
use Concerns\Fallback;
use Concerns\Interactivity;
use Concerns\Themes;
/**
* The current state of the prompt.
*/
public string $state = 'initial';
/**
* The error message from the validator.
*/
public string $error = '';
/**
* The cancel message displayed when this prompt is cancelled.
*/
public string $cancelMessage = 'Cancelled.';
/**
* The previously rendered frame.
*/
protected string $prevFrame = '';
/**
* How many new lines were written by the last output.
*/
protected int $newLinesWritten = 1;
/**
* Whether user input is required.
*/
public bool|string $required;
/**
* The validator callback or rules.
*/
public mixed $validate;
/**
* The cancellation callback.
*/
protected static ?Closure $cancelUsing;
/**
* Indicates if the prompt has been validated.
*/
protected bool $validated = false;
/**
* The custom validation callback.
*/
protected static ?Closure $validateUsing;
/**
* The revert handler from the StepBuilder.
*/
protected static ?Closure $revertUsing = null;
/**
* The output instance.
*/
protected static OutputInterface $output;
/**
* The terminal instance.
*/
protected static Terminal $terminal;
/**
* Get the value of the prompt.
*/
abstract public function value(): mixed;
/**
* Render the prompt and listen for input.
*/
public function prompt(): mixed
{
try {
$this->capturePreviousNewLines();
if (static::shouldFallback()) {
return $this->fallback();
}
static::$interactive ??= stream_isatty(STDIN);
if (! static::$interactive) {
return $this->default();
}
$this->checkEnvironment();
try {
static::terminal()->setTty('-icanon -isig -echo');
} catch (Throwable $e) {
static::output()->writeln("<comment>{$e->getMessage()}</comment>");
static::fallbackWhen(true);
return $this->fallback();
}
$this->hideCursor();
$this->render();
while (($key = static::terminal()->read()) !== null) {
$continue = $this->handleKeyPress($key);
$this->render();
if ($continue === false || $key === Key::CTRL_C) {
if ($key === Key::CTRL_C) {
if (isset(static::$cancelUsing)) {
return (static::$cancelUsing)();
} else {
static::terminal()->exit();
}
}
if ($key === Key::CTRL_U && self::$revertUsing) {
throw new FormRevertedException();
}
return $this->value();
}
}
} finally {
$this->clearListeners();
}
}
/**
* Register a callback to be invoked when a user cancels a prompt.
*/
public static function cancelUsing(?Closure $callback): void
{
static::$cancelUsing = $callback;
}
/**
* How many new lines were written by the last output.
*/
public function newLinesWritten(): int
{
return $this->newLinesWritten;
}
/**
* Capture the number of new lines written by the last output.
*/
protected function capturePreviousNewLines(): void
{
$this->newLinesWritten = method_exists(static::output(), 'newLinesWritten')
? static::output()->newLinesWritten()
: 1;
}
/**
* Set the output instance.
*/
public static function setOutput(OutputInterface $output): void
{
self::$output = $output;
}
/**
* Get the current output instance.
*/
protected static function output(): OutputInterface
{
return self::$output ??= new ConsoleOutput();
}
/**
* Write output directly, bypassing newline capture.
*/
protected static function writeDirectly(string $message): void
{
match (true) {
method_exists(static::output(), 'writeDirectly') => static::output()->writeDirectly($message),
method_exists(static::output(), 'getOutput') => static::output()->getOutput()->write($message),
default => static::output()->write($message),
};
}
/**
* Get the terminal instance.
*/
public static function terminal(): Terminal
{
return static::$terminal ??= new Terminal();
}
/**
* Set the custom validation callback.
*/
public static function validateUsing(Closure $callback): void
{
static::$validateUsing = $callback;
}
/**
* Revert the prompt using the given callback.
*
* @internal
*/
public static function revertUsing(Closure $callback): void
{
static::$revertUsing = $callback;
}
/**
* Clear any previous revert callback.
*
* @internal
*/
public static function preventReverting(): void
{
static::$revertUsing = null;
}
/**
* Render the prompt.
*/
protected function render(): void
{
$this->terminal()->initDimensions();
$frame = $this->renderTheme();
if ($frame === $this->prevFrame) {
return;
}
if ($this->state === 'initial') {
static::output()->write($frame);
$this->state = 'active';
$this->prevFrame = $frame;
return;
}
$terminalHeight = $this->terminal()->lines();
$previousFrameHeight = count(explode(PHP_EOL, $this->prevFrame));
$renderableLines = array_slice(explode(PHP_EOL, $frame), abs(min(0, $terminalHeight - $previousFrameHeight)));
$this->moveCursorToColumn(1);
$this->moveCursorUp(min($terminalHeight, $previousFrameHeight) - 1);
$this->eraseDown();
$this->output()->write(implode(PHP_EOL, $renderableLines));
$this->prevFrame = $frame;
}
/**
* Submit the prompt.
*/
protected function submit(): void
{
$this->validate($this->value());
if ($this->state !== 'error') {
$this->state = 'submit';
}
}
/**
* Handle a key press and determine whether to continue.
*/
private function handleKeyPress(string $key): bool
{
if ($this->state === 'error') {
$this->state = 'active';
}
$this->emit('key', $key);
if ($this->state === 'submit') {
return false;
}
if ($key === Key::CTRL_U) {
if (! self::$revertUsing) {
$this->state = 'error';
$this->error = 'This cannot be reverted.';
return true;
}
$this->state = 'cancel';
$this->cancelMessage = 'Reverted.';
call_user_func(self::$revertUsing);
return false;
}
if ($key === Key::CTRL_C) {
$this->state = 'cancel';
return false;
}
if ($this->validated) {
$this->validate($this->value());
}
return true;
}
/**
* Validate the input.
*/
private function validate(mixed $value): void
{
$this->validated = true;
if ($this->required !== false && $this->isInvalidWhenRequired($value)) {
$this->state = 'error';
$this->error = is_string($this->required) && strlen($this->required) > 0 ? $this->required : 'Required.';
return;
}
if (! isset($this->validate) && ! isset(static::$validateUsing)) {
return;
}
$error = match (true) {
is_callable($this->validate) => ($this->validate)($value),
isset(static::$validateUsing) => (static::$validateUsing)($this),
default => throw new RuntimeException('The validation logic is missing.'),
};
if (! is_string($error) && ! is_null($error)) {
throw new RuntimeException('The validator must return a string or null.');
}
if (is_string($error) && strlen($error) > 0) {
$this->state = 'error';
$this->error = $error;
}
}
/**
* Determine whether the given value is invalid when the prompt is required.
*/
protected function isInvalidWhenRequired(mixed $value): bool
{
return $value === '' || $value === [] || $value === false || $value === null;
}
/**
* Check whether the environment can support the prompt.
*/
private function checkEnvironment(): void
{
if (PHP_OS_FAMILY === 'Windows') {
throw new RuntimeException('Prompts is not currently supported on Windows. Please use WSL or configure a fallback.');
}
}
/**
* Restore the cursor and terminal state.
*/
public function __destruct()
{
$this->restoreCursor();
static::terminal()->restoreTty();
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace Laravel\Prompts;
use Closure;
use InvalidArgumentException;
class SearchPrompt extends Prompt
{
use Concerns\Scrolling;
use Concerns\Truncation;
use Concerns\TypedValue;
/**
* The cached matches.
*
* @var array<int|string, string>|null
*/
protected ?array $matches = null;
/**
* Create a new SearchPrompt 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 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->trackTypedValue(submit: false, ignore: fn ($key) => Key::oneOf([Key::HOME, Key::END, Key::CTRL_A, Key::CTRL_E], $key) && $this->highlighted !== null);
$this->initializeScrolling(null);
$this->on('key', fn ($key) => match ($key) {
Key::UP, Key::UP_ARROW, Key::SHIFT_TAB, Key::CTRL_P => $this->highlightPrevious(count($this->matches), true),
Key::DOWN, Key::DOWN_ARROW, Key::TAB, Key::CTRL_N => $this->highlightNext(count($this->matches), true),
Key::oneOf([Key::HOME, Key::CTRL_A], $key) => $this->highlighted !== null ? $this->highlight(0) : null,
Key::oneOf([Key::END, Key::CTRL_E], $key) => $this->highlighted !== null ? $this->highlight(count($this->matches()) - 1) : null,
Key::ENTER => $this->highlighted !== null ? $this->submit() : $this->search(),
Key::oneOf([Key::LEFT, Key::LEFT_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::CTRL_B, Key::CTRL_F], $key) => $this->highlighted = null,
default => $this->search(),
});
}
/**
* Perform the search.
*/
protected function search(): void
{
$this->state = 'searching';
$this->highlighted = null;
$this->render();
$this->matches = null;
$this->firstVisible = 0;
$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);
}
/**
* The currently visible matches.
*
* @return array<string>
*/
public function visible(): array
{
return array_slice($this->matches(), $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Get the current search query.
*/
public function searchValue(): string
{
return $this->typedValue;
}
/**
* Get the selected value.
*/
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;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace Laravel\Prompts;
use Illuminate\Support\Collection;
use InvalidArgumentException;
class SelectPrompt extends Prompt
{
use Concerns\Scrolling;
/**
* The options for the select prompt.
*
* @var array<int|string, string>
*/
public array $options;
/**
* Create a new SelectPrompt instance.
*
* @param array<int|string, string>|Collection<int|string, string> $options
*/
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;
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, 'k', 'h' => $this->highlightPrevious(count($this->options)),
Key::DOWN, Key::DOWN_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::TAB, Key::CTRL_N, Key::CTRL_F, 'j', 'l' => $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),
Key::ENTER => $this->submit(),
default => null,
});
}
/**
* Get the selected value.
*/
public function value(): int|string|null
{
if (static::$interactive === false) {
return $this->default;
}
if (array_is_list($this->options)) {
return $this->options[$this->highlighted] ?? null;
} else {
return array_keys($this->options)[$this->highlighted];
}
}
/**
* Get the selected label.
*/
public function label(): ?string
{
if (array_is_list($this->options)) {
return $this->options[$this->highlighted] ?? null;
} else {
return $this->options[array_keys($this->options)[$this->highlighted]] ?? null;
}
}
/**
* The currently visible options.
*
* @return array<int|string, string>
*/
public function visible(): array
{
return array_slice($this->options, $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Determine whether the given value is invalid when the prompt is required.
*/
protected function isInvalidWhenRequired(mixed $value): bool
{
return $value === null;
}
}

160
vendor/laravel/prompts/src/Spinner.php vendored Normal file
View File

@ -0,0 +1,160 @@
<?php
namespace Laravel\Prompts;
use Closure;
use RuntimeException;
class Spinner extends Prompt
{
/**
* How long to wait between rendering each frame.
*/
public int $interval = 100;
/**
* The number of times the spinner has been rendered.
*/
public int $count = 0;
/**
* Whether the spinner can only be rendered once.
*/
public bool $static = false;
/**
* The process ID after forking.
*/
protected int $pid;
/**
* Create a new Spinner instance.
*/
public function __construct(public string $message = '')
{
//
}
/**
* Render the spinner and execute the callback.
*
* @template TReturn of mixed
*
* @param \Closure(): TReturn $callback
* @return TReturn
*/
public function spin(Closure $callback): mixed
{
$this->capturePreviousNewLines();
if (! function_exists('pcntl_fork')) {
return $this->renderStatically($callback);
}
$originalAsync = pcntl_async_signals(true);
pcntl_signal(SIGINT, fn () => exit());
try {
$this->hideCursor();
$this->render();
$this->pid = pcntl_fork();
if ($this->pid === 0) {
while (true) { // @phpstan-ignore-line
$this->render();
$this->count++;
usleep($this->interval * 1000);
}
} else {
$result = $callback();
$this->resetTerminal($originalAsync);
return $result;
}
} catch (\Throwable $e) {
$this->resetTerminal($originalAsync);
throw $e;
}
}
/**
* Reset the terminal.
*/
protected function resetTerminal(bool $originalAsync): void
{
pcntl_async_signals($originalAsync);
pcntl_signal(SIGINT, SIG_DFL);
$this->eraseRenderedLines();
}
/**
* Render a static version of the spinner.
*
* @template TReturn of mixed
*
* @param \Closure(): TReturn $callback
* @return TReturn
*/
protected function renderStatically(Closure $callback): mixed
{
$this->static = true;
try {
$this->hideCursor();
$this->render();
$result = $callback();
} finally {
$this->eraseRenderedLines();
}
return $result;
}
/**
* Disable prompting for input.
*
* @throws \RuntimeException
*/
public function prompt(): never
{
throw new RuntimeException('Spinner cannot be prompted.');
}
/**
* Get the current value of the prompt.
*/
public function value(): bool
{
return true;
}
/**
* Clear the lines rendered by the spinner.
*/
protected function eraseRenderedLines(): void
{
$lines = explode(PHP_EOL, $this->prevFrame);
$this->moveCursor(-999, -count($lines) + 1);
$this->eraseDown();
}
/**
* Clean up after the spinner.
*/
public function __destruct()
{
if (! empty($this->pid)) {
posix_kill($this->pid, SIGHUP);
}
parent::__destruct();
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Laravel\Prompts;
use Closure;
use Illuminate\Support\Collection;
class SuggestPrompt extends Prompt
{
use Concerns\Scrolling;
use Concerns\Truncation;
use Concerns\TypedValue;
/**
* The options for the suggest prompt.
*
* @var array<string>|Closure(string): (array<string>|Collection<int, string>)
*/
public array|Closure $options;
/**
* The cache of matches.
*
* @var array<string>|null
*/
protected ?array $matches = null;
/**
* Create a new SuggestPrompt instance.
*
* @param array<string>|Collection<int, string>|Closure(string): (array<string>|Collection<int, string>) $options
*/
public function __construct(
public string $label,
array|Collection|Closure $options,
public string $placeholder = '',
public string $default = '',
public int $scroll = 5,
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->options = $options instanceof Collection ? $options->all() : $options;
$this->initializeScrolling(null);
$this->on('key', fn ($key) => match ($key) {
Key::UP, Key::UP_ARROW, Key::SHIFT_TAB, Key::CTRL_P => $this->highlightPrevious(count($this->matches()), true),
Key::DOWN, Key::DOWN_ARROW, Key::TAB, Key::CTRL_N => $this->highlightNext(count($this->matches()), true),
Key::oneOf([Key::HOME, Key::CTRL_A], $key) => $this->highlighted !== null ? $this->highlight(0) : null,
Key::oneOf([Key::END, Key::CTRL_E], $key) => $this->highlighted !== null ? $this->highlight(count($this->matches()) - 1) : null,
Key::ENTER => $this->selectHighlighted(),
Key::oneOf([Key::LEFT, Key::LEFT_ARROW, Key::RIGHT, Key::RIGHT_ARROW, Key::CTRL_B, Key::CTRL_F], $key) => $this->highlighted = null,
default => (function () {
$this->highlighted = null;
$this->matches = null;
$this->firstVisible = 0;
})(),
});
$this->trackTypedValue($default, ignore: fn ($key) => Key::oneOf([Key::HOME, Key::END, Key::CTRL_A, Key::CTRL_E], $key) && $this->highlighted !== null);
}
/**
* Get the entered value with a virtual cursor.
*/
public function valueWithCursor(int $maxWidth): string
{
if ($this->highlighted !== null) {
return $this->value() === ''
? $this->dim($this->truncate($this->placeholder, $maxWidth))
: $this->truncate($this->value(), $maxWidth);
}
if ($this->value() === '') {
return $this->dim($this->addCursor($this->placeholder, 0, $maxWidth));
}
return $this->addCursor($this->value(), $this->cursorPosition, $maxWidth);
}
/**
* Get options that match the input.
*
* @return array<string>
*/
public function matches(): array
{
if (is_array($this->matches)) {
return $this->matches;
}
if ($this->options instanceof Closure) {
$matches = ($this->options)($this->value());
return $this->matches = array_values($matches instanceof Collection ? $matches->all() : $matches);
}
return $this->matches = array_values(array_filter($this->options, function ($option) {
return str_starts_with(strtolower($option), strtolower($this->value()));
}));
}
/**
* The current visible matches.
*
* @return array<string>
*/
public function visible(): array
{
return array_slice($this->matches(), $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Select the highlighted entry.
*/
protected function selectHighlighted(): void
{
if ($this->highlighted === null) {
return;
}
$this->typedValue = $this->matches()[$this->highlighted];
}
}

71
vendor/laravel/prompts/src/Table.php vendored Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace Laravel\Prompts;
use Illuminate\Support\Collection;
class Table extends Prompt
{
/**
* The table headers.
*
* @var array<int, string|array<int, string>>
*/
public array $headers;
/**
* The table rows.
*
* @var array<int, array<int, string>>
*/
public array $rows;
/**
* Create a new Table instance.
*
* @param array<int, string|array<int, string>>|Collection<int, string|array<int, string>> $headers
* @param array<int, array<int, string>>|Collection<int, array<int, string>> $rows
*
* @phpstan-param ($rows is null ? list<list<string>>|Collection<int, list<string>> : list<string|list<string>>|Collection<int, string|list<string>>) $headers
*/
public function __construct(array|Collection $headers = [], array|Collection|null $rows = null)
{
if ($rows === null) {
$rows = $headers;
$headers = [];
}
$this->headers = $headers instanceof Collection ? $headers->all() : $headers;
$this->rows = $rows instanceof Collection ? $rows->all() : $rows;
}
/**
* Display the table.
*/
public function display(): void
{
$this->prompt();
}
/**
* Display the table.
*/
public function prompt(): bool
{
$this->capturePreviousNewLines();
$this->state = 'submit';
static::output()->write($this->renderTheme());
return true;
}
/**
* Get the value of the prompt.
*/
public function value(): bool
{
return true;
}
}

119
vendor/laravel/prompts/src/Terminal.php vendored Normal file
View File

@ -0,0 +1,119 @@
<?php
namespace Laravel\Prompts;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\Console\Terminal as SymfonyTerminal;
class Terminal
{
/**
* The initial TTY mode.
*/
protected ?string $initialTtyMode;
/**
* The Symfony Terminal instance.
*/
protected SymfonyTerminal $terminal;
/**
* Create a new Terminal instance.
*/
public function __construct()
{
$this->terminal = new SymfonyTerminal();
}
/**
* Read a line from the terminal.
*/
public function read(): string
{
$input = fread(STDIN, 1024);
return $input !== false ? $input : '';
}
/**
* Set the TTY mode.
*/
public function setTty(string $mode): void
{
$this->initialTtyMode ??= $this->exec('stty -g');
$this->exec("stty $mode");
}
/**
* Restore the initial TTY mode.
*/
public function restoreTty(): void
{
if (isset($this->initialTtyMode)) {
$this->exec("stty {$this->initialTtyMode}");
$this->initialTtyMode = null;
}
}
/**
* Get the number of columns in the terminal.
*/
public function cols(): int
{
return $this->terminal->getWidth();
}
/**
* Get the number of lines in the terminal.
*/
public function lines(): int
{
return $this->terminal->getHeight();
}
/**
* (Re)initialize the terminal dimensions.
*/
public function initDimensions(): void
{
(new ReflectionClass($this->terminal))
->getMethod('initDimensions')
->invoke($this->terminal);
}
/**
* Exit the interactive session.
*/
public function exit(): void
{
exit(1);
}
/**
* Execute the given command and return the output.
*/
protected function exec(string $command): string
{
$process = proc_open($command, [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes);
if (! $process) {
throw new RuntimeException('Failed to create process.');
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
$code = proc_close($process);
if ($code !== 0 || $stdout === false) {
throw new RuntimeException(trim($stderr ?: "Unknown error (code: $code)"), $code);
}
return $stdout;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Laravel\Prompts;
class TextPrompt extends Prompt
{
use Concerns\TypedValue;
/**
* Create a new TextPrompt instance.
*/
public function __construct(
public string $label,
public string $placeholder = '',
public string $default = '',
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
) {
$this->trackTypedValue($default);
}
/**
* Get the entered value with a virtual cursor.
*/
public function valueWithCursor(int $maxWidth): string
{
if ($this->value() === '') {
return $this->dim($this->addCursor($this->placeholder, 0, $maxWidth));
}
return $this->addCursor($this->value(), $this->cursorPosition, $maxWidth);
}
}

View File

@ -0,0 +1,245 @@
<?php
namespace Laravel\Prompts;
class TextareaPrompt extends Prompt
{
use Concerns\Scrolling;
use Concerns\Truncation;
use Concerns\TypedValue;
/**
* The width of the textarea.
*/
public int $width = 60;
/**
* Create a new TextareaPrompt instance.
*/
public function __construct(
public string $label,
public string $placeholder = '',
public string $default = '',
public bool|string $required = false,
public mixed $validate = null,
public string $hint = '',
int $rows = 5,
) {
$this->scroll = $rows;
$this->initializeScrolling();
$this->trackTypedValue(
default: $default,
submit: false,
allowNewLine: true,
);
$this->on('key', function ($key) {
if ($key[0] === "\e") {
match ($key) {
Key::UP, Key::UP_ARROW, Key::CTRL_P => $this->handleUpKey(),
Key::DOWN, Key::DOWN_ARROW, Key::CTRL_N => $this->handleDownKey(),
default => null,
};
return;
}
// Keys may be buffered.
foreach (mb_str_split($key) as $key) {
if ($key === Key::CTRL_D) {
$this->submit();
return;
}
}
});
}
/**
* Get the formatted value with a virtual cursor.
*/
public function valueWithCursor(): string
{
if ($this->value() === '') {
return $this->wrappedPlaceholderWithCursor();
}
return $this->addCursor($this->wrappedValue(), $this->cursorPosition + $this->cursorOffset(), -1);
}
/**
* The word-wrapped version of the typed value.
*/
public function wrappedValue(): string
{
return $this->mbWordwrap($this->value(), $this->width, PHP_EOL, true);
}
/**
* The formatted lines.
*
* @return array<int, string>
*/
public function lines(): array
{
return explode(PHP_EOL, $this->wrappedValue());
}
/**
* The currently visible lines.
*
* @return array<int, string>
*/
public function visible(): array
{
$this->adjustVisibleWindow();
$withCursor = $this->valueWithCursor();
return array_slice(explode(PHP_EOL, $withCursor), $this->firstVisible, $this->scroll, preserve_keys: true);
}
/**
* Handle the up key press.
*/
protected function handleUpKey(): void
{
if ($this->cursorPosition === 0) {
return;
}
$lines = collect($this->lines());
// Line length + 1 for the newline character
$lineLengths = $lines->map(fn ($line, $index) => mb_strwidth($line) + ($index === $lines->count() - 1 ? 0 : 1));
$currentLineIndex = $this->currentLineIndex();
if ($currentLineIndex === 0) {
// They're already at the first line, jump them to the first position
$this->cursorPosition = 0;
return;
}
$currentLines = $lineLengths->slice(0, $currentLineIndex + 1);
$currentColumn = $currentLines->last() - ($currentLines->sum() - $this->cursorPosition);
$destinationLineLength = ($lineLengths->get($currentLineIndex - 1) ?? $currentLines->first()) - 1;
$newColumn = min($destinationLineLength, $currentColumn);
$fullLines = $currentLines->slice(0, -2);
$this->cursorPosition = $fullLines->sum() + $newColumn;
}
/**
* Handle the down key press.
*/
protected function handleDownKey(): void
{
$lines = collect($this->lines());
// Line length + 1 for the newline character
$lineLengths = $lines->map(fn ($line, $index) => mb_strwidth($line) + ($index === $lines->count() - 1 ? 0 : 1));
$currentLineIndex = $this->currentLineIndex();
if ($currentLineIndex === $lines->count() - 1) {
// They're already at the last line, jump them to the last position
$this->cursorPosition = mb_strwidth($lines->implode(PHP_EOL));
return;
}
// Lines up to and including the current line
$currentLines = $lineLengths->slice(0, $currentLineIndex + 1);
$currentColumn = $currentLines->last() - ($currentLines->sum() - $this->cursorPosition);
$destinationLineLength = $lineLengths->get($currentLineIndex + 1) ?? $currentLines->last();
if ($currentLineIndex + 1 !== $lines->count() - 1) {
$destinationLineLength--;
}
$newColumn = min(max(0, $destinationLineLength), $currentColumn);
$this->cursorPosition = $currentLines->sum() + $newColumn;
}
/**
* Adjust the visible window to ensure the cursor is always visible.
*/
protected function adjustVisibleWindow(): void
{
if (count($this->lines()) < $this->scroll) {
return;
}
$currentLineIndex = $this->currentLineIndex();
while ($this->firstVisible + $this->scroll <= $currentLineIndex) {
$this->firstVisible++;
}
if ($currentLineIndex === $this->firstVisible - 1) {
$this->firstVisible = max(0, $this->firstVisible - 1);
}
// Make sure there are always the scroll amount visible
if ($this->firstVisible + $this->scroll > count($this->lines())) {
$this->firstVisible = count($this->lines()) - $this->scroll;
}
}
/**
* Get the index of the current line that the cursor is on.
*/
protected function currentLineIndex(): int
{
$totalLineLength = 0;
return (int) collect($this->lines())->search(function ($line) use (&$totalLineLength) {
$totalLineLength += mb_strwidth($line) + 1;
return $totalLineLength > $this->cursorPosition;
}) ?: 0;
}
/**
* Calculate the cursor offset considering wrapped words.
*/
protected function cursorOffset(): int
{
$cursorOffset = 0;
preg_match_all('/\S{'.$this->width.',}/u', $this->value(), $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
if ($this->cursorPosition + $cursorOffset >= $match[1] + mb_strwidth($match[0])) {
$cursorOffset += (int) floor(mb_strwidth($match[0]) / $this->width);
}
}
return $cursorOffset;
}
/**
* A wrapped version of the placeholder with the virtual cursor.
*/
protected function wrappedPlaceholderWithCursor(): string
{
return implode(PHP_EOL, array_map(
$this->dim(...),
explode(PHP_EOL, $this->addCursor(
$this->mbWordwrap($this->placeholder, $this->width, PHP_EOL, true),
cursorPosition: 0,
))
));
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Laravel\Prompts\Themes\Contracts;
interface Scrolling
{
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int;
}

View File

@ -0,0 +1,98 @@
<?php
namespace Laravel\Prompts\Themes\Default\Concerns;
use Laravel\Prompts\Prompt;
trait DrawsBoxes
{
protected int $minWidth = 60;
/**
* Draw a box.
*
* @return $this
*/
protected function box(
string $title,
string $body,
string $footer = '',
string $color = 'gray',
string $info = '',
): self {
$this->minWidth = min($this->minWidth, Prompt::terminal()->cols() - 6);
$bodyLines = collect(explode(PHP_EOL, $body));
$footerLines = collect(explode(PHP_EOL, $footer))->filter();
$width = $this->longest(
$bodyLines
->merge($footerLines)
->push($title)
->toArray()
);
$titleLength = mb_strwidth($this->stripEscapeSequences($title));
$titleLabel = $titleLength > 0 ? " {$title} " : '';
$topBorder = str_repeat('─', $width - $titleLength + ($titleLength > 0 ? 0 : 2));
$this->line("{$this->{$color}(' ┌')}{$titleLabel}{$this->{$color}($topBorder.'┐')}");
$bodyLines->each(function ($line) use ($width, $color) {
$this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}");
});
if ($footerLines->isNotEmpty()) {
$this->line($this->{$color}(' ├'.str_repeat('─', $width + 2).'┤'));
$footerLines->each(function ($line) use ($width, $color) {
$this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}");
});
}
$this->line($this->{$color}(' └'.str_repeat(
'─', $info ? ($width - mb_strwidth($this->stripEscapeSequences($info))) : ($width + 2)
).($info ? " {$info} " : '').'┘'));
return $this;
}
/**
* Get the length of the longest line.
*
* @param array<string> $lines
*/
protected function longest(array $lines, int $padding = 0): int
{
return max(
$this->minWidth,
collect($lines)
->map(fn ($line) => mb_strwidth($this->stripEscapeSequences($line)) + $padding)
->max()
);
}
/**
* Pad text ignoring ANSI escape sequences.
*/
protected function pad(string $text, int $length): string
{
$rightPadding = str_repeat(' ', max(0, $length - mb_strwidth($this->stripEscapeSequences($text))));
return "{$text}{$rightPadding}";
}
/**
* Strip ANSI escape sequences from the given text.
*/
protected function stripEscapeSequences(string $text): string
{
// Strip ANSI escape sequences.
$text = preg_replace("/\e[^m]*m/", '', $text);
// Strip Symfony named style tags.
$text = preg_replace("/<(info|comment|question|error)>(.*?)<\/\\1>/", '$2', $text);
// Strip Symfony inline style tags.
return preg_replace("/<(?:(?:[fb]g|options)=[a-z,;]+)+>(.*?)<\/>/i", '$1', $text);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Laravel\Prompts\Themes\Default\Concerns;
use Illuminate\Support\Collection;
trait DrawsScrollbars
{
/**
* Render a scrollbar beside the visible items.
*
* @param \Illuminate\Support\Collection<int, string> $visible
* @return \Illuminate\Support\Collection<int, string>
*/
protected function scrollbar(Collection $visible, int $firstVisible, int $height, int $total, int $width, string $color = 'cyan'): Collection
{
if ($height >= $total) {
return $visible;
}
$scrollPosition = $this->scrollPosition($firstVisible, $height, $total);
return $visible
->values()
->map(fn ($line) => $this->pad($line, $width))
->map(fn ($line, $index) => match ($index) {
$scrollPosition => preg_replace('/.$/', $this->{$color}('┃'), $line),
default => preg_replace('/.$/', $this->gray('│'), $line),
});
}
/**
* Return the position where the scrollbar "handle" should be rendered.
*/
protected function scrollPosition(int $firstVisible, int $height, int $total): int
{
if ($firstVisible === 0) {
return 0;
}
$maxPosition = $total - $height;
if ($firstVisible === $maxPosition) {
return $height - 1;
}
if ($height <= 2) {
return -1;
}
$percent = $firstVisible / $maxPosition;
return (int) round($percent * ($height - 3)) + 1;
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\ConfirmPrompt;
class ConfirmPromptRenderer extends Renderer
{
use Concerns\DrawsBoxes;
/**
* Render the confirm prompt.
*/
public function __invoke(ConfirmPrompt $prompt): string
{
return match ($prompt->state) {
'submit' => $this
->box(
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->truncate($prompt->label(), $prompt->terminal()->cols() - 6)
),
'cancel' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'red'
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'yellow',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->renderOptions($prompt),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
),
};
}
/**
* Render the confirm prompt options.
*/
protected function renderOptions(ConfirmPrompt $prompt): string
{
$length = (int) floor(($prompt->terminal()->cols() - 14) / 2);
$yes = $this->truncate($prompt->yes, $length);
$no = $this->truncate($prompt->no, $length);
if ($prompt->state === 'cancel') {
return $this->dim($prompt->confirmed
? "{$this->strikethrough($yes)} / ○ {$this->strikethrough($no)}"
: "{$this->strikethrough($yes)} / ● {$this->strikethrough($no)}");
}
return $prompt->confirmed
? "{$this->green('●')} {$yes} {$this->dim('/ ○ '.$no)}"
: "{$this->dim('○ '.$yes.' /')} {$this->green('●')} {$no}";
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\MultiSearchPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class MultiSearchPromptRenderer extends Renderer implements Scrolling
{
use Concerns\DrawsBoxes;
use Concerns\DrawsScrollbars;
/**
* Render the suggest prompt.
*/
public function __invoke(MultiSearchPrompt $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->renderSelectedOptions($prompt),
),
'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($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$prompt->valueWithCursor($maxWidth),
$this->renderOptions($prompt),
color: 'yellow',
info: $this->getInfoText($prompt),
)
->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),
info: $this->getInfoText($prompt),
)
->hint($prompt->hint),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$prompt->valueWithCursor($maxWidth),
$this->renderOptions($prompt),
info: $this->getInfoText($prompt),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
)
->spaceForDropdown($prompt)
};
}
/**
* Render the value with the cursor and a search icon.
*/
protected function valueWithCursorAndSearchIcon(MultiSearchPrompt $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(MultiSearchPrompt $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(MultiSearchPrompt $prompt): string
{
if ($prompt->searchValue() !== '' && empty($prompt->matches())) {
return $this->gray(' '.($prompt->state === 'searching' ? 'Searching...' : 'No results.'));
}
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 12))
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->matches()));
$active = $index === $prompt->highlighted;
$selected = $prompt->isList()
? in_array($label, $prompt->value())
: in_array($key, $prompt->value());
return match (true) {
$active && $selected => "{$this->cyan(' ◼')} {$label} ",
$active => "{$this->cyan('')}{$label} ",
$selected => " {$this->cyan('◼')} {$this->dim($label)} ",
default => " {$this->dim('◻')} {$this->dim($label)} ",
};
}),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->matches()),
min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6)
)->implode(PHP_EOL);
}
/**
* Render the selected options.
*/
protected function renderSelectedOptions(MultiSearchPrompt $prompt): string
{
if (count($prompt->labels()) === 0) {
return $this->gray('None');
}
return implode("\n", array_map(
fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 6),
$prompt->labels()
));
}
/**
* Render the info text.
*/
protected function getInfoText(MultiSearchPrompt $prompt): string
{
$info = count($prompt->value()).' selected';
$hiddenCount = count($prompt->value()) - collect($prompt->matches())
->filter(fn ($label, $key) => in_array($prompt->isList() ? $label : $key, $prompt->value()))
->count();
if ($hiddenCount > 0) {
$info .= " ($hiddenCount hidden)";
}
return $info;
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 7;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\MultiSelectPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class MultiSelectPromptRenderer extends Renderer implements Scrolling
{
use Concerns\DrawsBoxes;
use Concerns\DrawsScrollbars;
/**
* Render the multiselect prompt.
*/
public function __invoke(MultiSelectPrompt $prompt): string
{
return match ($prompt->state) {
'submit' => $this
->box(
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->renderSelectedOptions($prompt)
),
'cancel' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'yellow',
info: count($prompt->options) > $prompt->scroll ? (count($prompt->value()).' selected') : '',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->renderOptions($prompt),
info: count($prompt->options) > $prompt->scroll ? (count($prompt->value()).' selected') : '',
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
),
};
}
/**
* Render the options.
*/
protected function renderOptions(MultiSelectPrompt $prompt): string
{
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 12))
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->options));
$active = $index === $prompt->highlighted;
if (array_is_list($prompt->options)) {
$value = $prompt->options[$index];
} else {
$value = array_keys($prompt->options)[$index];
}
$selected = in_array($value, $prompt->value());
if ($prompt->state === 'cancel') {
return $this->dim(match (true) {
$active && $selected => "{$this->strikethrough($label)} ",
$active => "{$this->strikethrough($label)} ",
$selected => "{$this->strikethrough($label)} ",
default => "{$this->strikethrough($label)} ",
});
}
return match (true) {
$active && $selected => "{$this->cyan(' ◼')} {$label} ",
$active => "{$this->cyan('')}{$label} ",
$selected => " {$this->cyan('◼')} {$this->dim($label)} ",
default => " {$this->dim('◻')} {$this->dim($label)} ",
};
})
->values(),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->options),
min($this->longest($prompt->options, padding: 6), $prompt->terminal()->cols() - 6),
$prompt->state === 'cancel' ? 'dim' : 'cyan'
)->implode(PHP_EOL);
}
/**
* Render the selected options.
*/
protected function renderSelectedOptions(MultiSelectPrompt $prompt): string
{
if (count($prompt->labels()) === 0) {
return $this->gray('None');
}
return implode("\n", array_map(
fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 6),
$prompt->labels()
));
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 5;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\Note;
class NoteRenderer extends Renderer
{
/**
* Render the note.
*/
public function __invoke(Note $note): string
{
$lines = collect(explode(PHP_EOL, $note->message));
switch ($note->type) {
case 'intro':
case 'outro':
$lines = $lines->map(fn ($line) => " {$line} ");
$longest = $lines->map(fn ($line) => strlen($line))->max();
$lines
->each(function ($line) use ($longest) {
$line = str_pad($line, $longest, ' ');
$this->line(" {$this->bgCyan($this->black($line))}");
});
return $this;
case 'warning':
$lines->each(fn ($line) => $this->line($this->yellow(" {$line}")));
return $this;
case 'error':
$lines->each(fn ($line) => $this->line($this->red(" {$line}")));
return $this;
case 'alert':
$lines->each(fn ($line) => $this->line(" {$this->bgRed($this->white(" {$line} "))}"));
return $this;
case 'info':
$lines->each(fn ($line) => $this->line($this->green(" {$line}")));
return $this;
default:
$lines->each(fn ($line) => $this->line(" {$line}"));
return $this;
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\PasswordPrompt;
class PasswordPromptRenderer extends Renderer
{
use Concerns\DrawsBoxes;
/**
* Render the password prompt.
*/
public function __invoke(PasswordPrompt $prompt): string
{
$maxWidth = $prompt->terminal()->cols() - 6;
return match ($prompt->state) {
'submit' => $this
->box(
$this->dim($prompt->label),
$this->truncate($prompt->masked(), $maxWidth),
),
'cancel' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->strikethrough($this->dim($this->truncate($prompt->masked() ?: $prompt->placeholder, $maxWidth))),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$prompt->maskedWithCursor($maxWidth),
color: 'yellow',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$prompt->maskedWithCursor($maxWidth),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
),
};
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\PausePrompt;
class PausePromptRenderer extends Renderer
{
use Concerns\DrawsBoxes;
/**
* Render the pause prompt.
*/
public function __invoke(PausePrompt $prompt): string
{
match ($prompt->state) {
'submit' => collect(explode(PHP_EOL, $prompt->message))
->each(fn ($line) => $this->line($this->gray(" {$line}"))),
default => collect(explode(PHP_EOL, $prompt->message))
->each(fn ($line) => $this->line($this->green(" {$line}")))
};
return $this;
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\Progress;
class ProgressRenderer extends Renderer
{
use Concerns\DrawsBoxes;
/**
* The character to use for the progress bar.
*/
protected string $barCharacter = '█';
/**
* Render the progress bar.
*
* @param Progress<int|iterable<mixed>> $progress
*/
public function __invoke(Progress $progress): string
{
$filled = str_repeat($this->barCharacter, (int) ceil($progress->percentage() * min($this->minWidth, $progress->terminal()->cols() - 6)));
return match ($progress->state) {
'submit' => $this
->box(
$this->dim($this->truncate($progress->label, $progress->terminal()->cols() - 6)),
$this->dim($filled),
info: $progress->progress.'/'.$progress->total,
),
'error' => $this
->box(
$this->truncate($progress->label, $progress->terminal()->cols() - 6),
$this->dim($filled),
color: 'red',
info: $progress->progress.'/'.$progress->total,
),
'cancel' => $this
->box(
$this->truncate($progress->label, $progress->terminal()->cols() - 6),
$this->dim($filled),
color: 'red',
info: $progress->progress.'/'.$progress->total,
)
->error($progress->cancelMessage),
default => $this
->box(
$this->cyan($this->truncate($progress->label, $progress->terminal()->cols() - 6)),
$this->dim($filled),
info: $progress->progress.'/'.$progress->total,
)
->when(
$progress->hint,
fn () => $this->hint($progress->hint),
fn () => $this->newLine() // Space for errors
)
};
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\Concerns\Colors;
use Laravel\Prompts\Concerns\Truncation;
use Laravel\Prompts\Prompt;
abstract class Renderer
{
use Colors;
use Truncation;
/**
* The output to be rendered.
*/
protected string $output = '';
/**
* Create a new renderer instance.
*/
public function __construct(protected Prompt $prompt)
{
//
}
/**
* Render a line of output.
*/
protected function line(string $message): self
{
$this->output .= $message.PHP_EOL;
return $this;
}
/**
* Render a new line.
*/
protected function newLine(int $count = 1): self
{
$this->output .= str_repeat(PHP_EOL, $count);
return $this;
}
/**
* Render a warning message.
*/
protected function warning(string $message): self
{
return $this->line($this->yellow("{$message}"));
}
/**
* Render an error message.
*/
protected function error(string $message): self
{
return $this->line($this->red("{$message}"));
}
/**
* Render an hint message.
*/
protected function hint(string $message): self
{
if ($message === '') {
return $this;
}
$message = $this->truncate($message, $this->prompt->terminal()->cols() - 6);
return $this->line($this->gray(" {$message}"));
}
/**
* Apply the callback if the given "value" is truthy.
*
* @return $this
*/
protected function when(mixed $value, callable $callback, ?callable $default = null): self
{
if ($value) {
$callback($this);
} elseif ($default) {
$default($this);
}
return $this;
}
/**
* Render the output with a blank line above and below.
*/
public function __toString()
{
return str_repeat(PHP_EOL, max(2 - $this->prompt->newLinesWritten(), 0))
.$this->output
.(in_array($this->prompt->state, ['submit', 'cancel']) ? PHP_EOL : '');
}
}

View File

@ -0,0 +1,134 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\SearchPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class SearchPromptRenderer extends Renderer implements Scrolling
{
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($prompt->cancelMessage),
'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),
)
->hint($prompt->hint),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$prompt->valueWithCursor($maxWidth),
$this->renderOptions($prompt),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
)
->spaceForDropdown($prompt)
};
}
/**
* 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->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 10))
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->matches()));
return $prompt->highlighted === $index
? "{$this->cyan('')} {$label} "
: " {$this->dim($label)} ";
})
->values(),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->matches()),
min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6)
)->implode(PHP_EOL);
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 7;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class SelectPromptRenderer extends Renderer implements Scrolling
{
use Concerns\DrawsBoxes;
use Concerns\DrawsScrollbars;
/**
* Render the select prompt.
*/
public function __invoke(SelectPrompt $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->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->renderOptions($prompt),
color: 'yellow',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->renderOptions($prompt),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
),
};
}
/**
* Render the options.
*/
protected function renderOptions(SelectPrompt $prompt): string
{
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 12))
->map(function ($label, $key) use ($prompt) {
$index = array_search($key, array_keys($prompt->options));
if ($prompt->state === 'cancel') {
return $this->dim($prompt->highlighted === $index
? "{$this->strikethrough($label)} "
: "{$this->strikethrough($label)} "
);
}
return $prompt->highlighted === $index
? "{$this->cyan('')} {$this->cyan('●')} {$label} "
: " {$this->dim('○')} {$this->dim($label)} ";
})
->values(),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->options),
min($this->longest($prompt->options, padding: 6), $prompt->terminal()->cols() - 6),
$prompt->state === 'cancel' ? 'dim' : 'cyan'
)->implode(PHP_EOL);
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 5;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\Spinner;
class SpinnerRenderer extends Renderer
{
/**
* The frames of the spinner.
*
* @var array<string>
*/
protected array $frames = ['⠂', '⠒', '⠐', '⠰', '⠠', '⠤', '⠄', '⠆'];
/**
* The frame to render when the spinner is static.
*/
protected string $staticFrame = '⠶';
/**
* The interval between frames.
*/
protected int $interval = 75;
/**
* Render the spinner.
*/
public function __invoke(Spinner $spinner): string
{
if ($spinner->static) {
return $this->line(" {$this->cyan($this->staticFrame)} {$spinner->message}");
}
$spinner->interval = $this->interval;
$frame = $this->frames[$spinner->count % count($this->frames)];
return $this->line(" {$this->cyan($frame)} {$spinner->message}");
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\SuggestPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class SuggestPromptRenderer extends Renderer implements Scrolling
{
use Concerns\DrawsBoxes;
use Concerns\DrawsScrollbars;
/**
* Render the suggest prompt.
*/
public function __invoke(SuggestPrompt $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->value(), $maxWidth),
),
'cancel' => $this
->box(
$this->dim($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->strikethrough($this->dim($this->truncate($prompt->value() ?: $prompt->placeholder, $maxWidth))),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->valueWithCursorAndArrow($prompt, $maxWidth),
$this->renderOptions($prompt),
color: 'yellow',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$this->valueWithCursorAndArrow($prompt, $maxWidth),
$this->renderOptions($prompt),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
)
->spaceForDropdown($prompt),
};
}
/**
* Render the value with the cursor and an arrow.
*/
protected function valueWithCursorAndArrow(SuggestPrompt $prompt, int $maxWidth): string
{
if ($prompt->highlighted !== null || $prompt->value() !== '' || count($prompt->matches()) === 0) {
return $prompt->valueWithCursor($maxWidth);
}
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(SuggestPrompt $prompt): self
{
if ($prompt->value() === '' && $prompt->highlighted === null) {
$this->newLine(min(
count($prompt->matches()),
$prompt->scroll,
$prompt->terminal()->lines() - 7
) + 1);
}
return $this;
}
/**
* Render the options.
*/
protected function renderOptions(SuggestPrompt $prompt): string
{
if (empty($prompt->matches()) || ($prompt->value() === '' && $prompt->highlighted === null)) {
return '';
}
return $this->scrollbar(
collect($prompt->visible())
->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 10))
->map(fn ($label, $key) => $prompt->highlighted === $key
? "{$this->cyan('')} {$label} "
: " {$this->dim($label)} "
),
$prompt->firstVisible,
$prompt->scroll,
count($prompt->matches()),
min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6),
$prompt->state === 'cancel' ? 'dim' : 'cyan'
)->implode(PHP_EOL);
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 7;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\Output\BufferedConsoleOutput;
use Laravel\Prompts\Table;
use Symfony\Component\Console\Helper\Table as SymfonyTable;
use Symfony\Component\Console\Helper\TableStyle;
class TableRenderer extends Renderer
{
/**
* Render the table.
*/
public function __invoke(Table $table): string
{
$tableStyle = (new TableStyle())
->setHorizontalBorderChars('─')
->setVerticalBorderChars('│', '│')
->setCellHeaderFormat($this->dim('<fg=default>%s</>'))
->setCellRowFormat('<fg=default>%s</>');
if (empty($table->headers)) {
$tableStyle->setCrossingChars('┼', '', '', '', '┤', '┘</>', '┴', '└', '├', '<fg=gray>┌', '┬', '┐');
} else {
$tableStyle->setCrossingChars('┼', '<fg=gray>┌', '┬', '┐', '┤', '┘</>', '┴', '└', '├');
}
$buffered = new BufferedConsoleOutput();
(new SymfonyTable($buffered))
->setHeaders($table->headers)
->setRows($table->rows)
->setStyle($tableStyle)
->render();
collect(explode(PHP_EOL, trim($buffered->content(), PHP_EOL)))
->each(fn ($line) => $this->line(' '.$line));
return $this;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\TextPrompt;
class TextPromptRenderer extends Renderer
{
use Concerns\DrawsBoxes;
/**
* Render the text prompt.
*/
public function __invoke(TextPrompt $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->value(), $maxWidth),
),
'cancel' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$this->strikethrough($this->dim($this->truncate($prompt->value() ?: $prompt->placeholder, $maxWidth))),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->terminal()->cols() - 6),
$prompt->valueWithCursor($maxWidth),
color: 'yellow',
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)),
$prompt->valueWithCursor($maxWidth),
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
)
};
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Laravel\Prompts\Themes\Default;
use Laravel\Prompts\TextareaPrompt;
use Laravel\Prompts\Themes\Contracts\Scrolling;
class TextareaPromptRenderer extends Renderer implements Scrolling
{
use Concerns\DrawsBoxes;
use Concerns\DrawsScrollbars;
/**
* Render the textarea prompt.
*/
public function __invoke(TextareaPrompt $prompt): string
{
$prompt->width = $prompt->terminal()->cols() - 8;
return match ($prompt->state) {
'submit' => $this
->box(
$this->dim($this->truncate($prompt->label, $prompt->width)),
collect($prompt->lines())->implode(PHP_EOL),
),
'cancel' => $this
->box(
$this->truncate($prompt->label, $prompt->width),
collect($prompt->lines())->map(fn ($line) => $this->strikethrough($this->dim($line)))->implode(PHP_EOL),
color: 'red',
)
->error($prompt->cancelMessage),
'error' => $this
->box(
$this->truncate($prompt->label, $prompt->width),
$this->renderText($prompt),
color: 'yellow',
info: 'Ctrl+D to submit'
)
->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)),
default => $this
->box(
$this->cyan($this->truncate($prompt->label, $prompt->width)),
$this->renderText($prompt),
info: 'Ctrl+D to submit'
)
->when(
$prompt->hint,
fn () => $this->hint($prompt->hint),
fn () => $this->newLine() // Space for errors
)
};
}
/**
* Render the text in the prompt.
*/
protected function renderText(TextareaPrompt $prompt): string
{
$visible = collect($prompt->visible());
while ($visible->count() < $prompt->scroll) {
$visible->push('');
}
$longest = $this->longest($prompt->lines()) + 2;
return $this->scrollbar(
$visible,
$prompt->firstVisible,
$prompt->scroll,
count($prompt->lines()),
min($longest, $prompt->width + 2),
)->implode(PHP_EOL);
}
/**
* The number of lines to reserve outside of the scrollable area.
*/
public function reservedLines(): int
{
return 5;
}
}

207
vendor/laravel/prompts/src/helpers.php vendored Normal file
View File

@ -0,0 +1,207 @@
<?php
namespace Laravel\Prompts;
use Closure;
use Illuminate\Support\Collection;
/**
* Prompt the user for text input.
*/
function text(string $label, string $placeholder = '', string $default = '', bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new TextPrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user for multiline text input.
*/
function textarea(string $label, string $placeholder = '', string $default = '', bool|string $required = false, ?Closure $validate = null, string $hint = '', int $rows = 5): string
{
return (new TextareaPrompt($label, $placeholder, $default, $required, $validate, $hint, $rows))->prompt();
}
/**
* Prompt the user for input, hiding the value.
*/
function password(string $label, string $placeholder = '', bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new PasswordPrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user to select an option.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param true|string $required
*/
function select(string $label, array|Collection $options, int|string|null $default = null, int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SelectPrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user to select multiple options.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param array<int|string>|Collection<int, int|string> $default
* @return array<int|string>
*/
function multiselect(string $label, array|Collection $options, array|Collection $default = [], int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.'): array
{
return (new MultiSelectPrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user to confirm an action.
*/
function confirm(string $label, bool $default = true, string $yes = 'Yes', string $no = 'No', bool|string $required = false, mixed $validate = null, string $hint = ''): bool
{
return (new ConfirmPrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user to continue or cancel after pausing.
*/
function pause(string $message = 'Press enter to continue...'): bool
{
return (new PausePrompt(...func_get_args()))->prompt();
}
/**
* Prompt the user for text input with auto-completion.
*
* @param array<string>|Collection<int, string>|Closure(string): array<string> $options
*/
function suggest(string $label, array|Collection|Closure $options, string $placeholder = '', string $default = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = ''): string
{
return (new SuggestPrompt(...func_get_args()))->prompt();
}
/**
* Allow the user to search for an option.
*
* @param Closure(string): array<int|string, string> $options
* @param true|string $required
*/
function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, mixed $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SearchPrompt(...func_get_args()))->prompt();
}
/**
* Allow the user to search for multiple option.
*
* @param Closure(string): array<int|string, string> $options
* @return array<int|string>
*/
function multisearch(string $label, Closure $options, string $placeholder = '', int $scroll = 5, bool|string $required = false, mixed $validate = null, string $hint = 'Use the space bar to select options.'): array
{
return (new MultiSearchPrompt(...func_get_args()))->prompt();
}
/**
* Render a spinner while the given callback is executing.
*
* @template TReturn of mixed
*
* @param \Closure(): TReturn $callback
* @return TReturn
*/
function spin(Closure $callback, string $message = ''): mixed
{
return (new Spinner($message))->spin($callback);
}
/**
* Display a note.
*/
function note(string $message, ?string $type = null): void
{
(new Note($message, $type))->display();
}
/**
* Display an error.
*/
function error(string $message): void
{
(new Note($message, 'error'))->display();
}
/**
* Display a warning.
*/
function warning(string $message): void
{
(new Note($message, 'warning'))->display();
}
/**
* Display an alert.
*/
function alert(string $message): void
{
(new Note($message, 'alert'))->display();
}
/**
* Display an informational message.
*/
function info(string $message): void
{
(new Note($message, 'info'))->display();
}
/**
* Display an introduction.
*/
function intro(string $message): void
{
(new Note($message, 'intro'))->display();
}
/**
* Display a closing message.
*/
function outro(string $message): void
{
(new Note($message, 'outro'))->display();
}
/**
* Display a table.
*
* @param array<int, string|array<int, string>>|Collection<int, string|array<int, string>> $headers
* @param array<int, array<int, string>>|Collection<int, array<int, string>> $rows
*/
function table(array|Collection $headers = [], array|Collection|null $rows = null): void
{
(new Table($headers, $rows))->display();
}
/**
* Display a progress bar.
*
* @template TSteps of iterable<mixed>|int
* @template TReturn
*
* @param TSteps $steps
* @param ?Closure((TSteps is int ? int : value-of<TSteps>), Progress<TSteps>): TReturn $callback
* @return ($callback is null ? Progress<TSteps> : array<TReturn>)
*/
function progress(string $label, iterable|int $steps, ?Closure $callback = null, string $hint = ''): array|Progress
{
$progress = new Progress($label, $steps, $hint);
if ($callback !== null) {
return $progress->map($callback);
}
return $progress;
}
function form(): FormBuilder
{
return new FormBuilder();
}