One Hat Cyber Team
Your IP:
216.73.216.30
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
self
/
root
/
proc
/
thread-self
/
cwd
/
View File Name :
prompts.tar
src/helpers.php 0000644 00000012724 15111220237 0007506 0 ustar 00 <?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, Closure $validate = null, string $hint = ''): string { return (new TextPrompt($label, $placeholder, $default, $required, $validate, $hint))->prompt(); } /** * Prompt the user for input, hiding the value. */ function password(string $label, string $placeholder = '', bool|string $required = false, Closure $validate = null, string $hint = ''): string { return (new PasswordPrompt($label, $placeholder, $required, $validate, $hint))->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 $default = null, int $scroll = 5, Closure $validate = null, string $hint = '', bool|string $required = true): int|string { return (new SelectPrompt($label, $options, $default, $scroll, $validate, $hint, $required))->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, Closure $validate = null, string $hint = 'Use the space bar to select options.'): array { return (new MultiSelectPrompt($label, $options, $default, $scroll, $required, $validate, $hint))->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, Closure $validate = null, string $hint = ''): bool { return (new ConfirmPrompt($label, $default, $yes, $no, $required, $validate, $hint))->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, Closure $validate = null, string $hint = ''): string { return (new SuggestPrompt($label, $options, $placeholder, $default, $scroll, $required, $validate, $hint))->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, Closure $validate = null, string $hint = '', bool|string $required = true): int|string { return (new SearchPrompt($label, $options, $placeholder, $scroll, $validate, $hint, $required))->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, Closure $validate = null, string $hint = 'Use the space bar to select options.'): array { return (new MultiSearchPrompt($label, $options, $placeholder, $scroll, $required, $validate, $hint))->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 $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; } src/Table.php 0000644 00000003114 15111220237 0007064 0 ustar 00 <?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 $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; } } src/Exceptions/NonInteractiveValidationException.php 0000644 00000000221 15111220237 0016774 0 ustar 00 <?php namespace Laravel\Prompts\Exceptions; use RuntimeException; class NonInteractiveValidationException extends RuntimeException { // } src/PasswordPrompt.php 0000644 00000001731 15111220237 0011044 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 ?Closure $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); } } src/SearchPrompt.php 0000644 00000007772 15111220237 0010462 0 ustar 00 <?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 ?Closure $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; } } src/Note.php 0000644 00000001276 15111220237 0006751 0 ustar 00 <?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; } } src/SelectPrompt.php 0000644 00000006332 15111220237 0010463 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 ?Closure $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; } } src/Spinner.php 0000644 00000006371 15111220237 0007463 0 ustar 00 <?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(); } } src/MultiSearchPrompt.php 0000644 00000011025 15111220237 0011457 0 ustar 00 <?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; /** * 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 ?Closure $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; } if (strlen($this->typedValue) === 0) { $matches = ($this->options)($this->typedValue); return $this->matches = [ ...array_diff($this->values, $matches), ...$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); } /** * Toggle the highlighted entry. */ protected function toggleHighlighted(): void { if (array_is_list($this->matches)) { $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); } } src/Output/BufferedConsoleOutput.php 0000644 00000001655 15111220237 0013633 0 ustar 00 <?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); } } src/Output/ConsoleOutput.php 0000644 00000002241 15111220237 0012160 0 ustar 00 <?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); } } src/Concerns/Themes.php 0000644 00000006747 15111220237 0011053 0 ustar 00 <?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\Progress; use Laravel\Prompts\SearchPrompt; use Laravel\Prompts\SelectPrompt; use Laravel\Prompts\Spinner; use Laravel\Prompts\SuggestPrompt; use Laravel\Prompts\Table; 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\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\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, PasswordPrompt::class => PasswordPromptRenderer::class, SelectPrompt::class => SelectPromptRenderer::class, MultiSelectPrompt::class => MultiSelectPromptRenderer::class, ConfirmPrompt::class => ConfirmPromptRenderer::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); } } src/Concerns/FakesInputOutput.php 0000644 00000005146 15111220237 0013110 0 ustar 00 <?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); 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()); } } src/Concerns/Events.php 0000644 00000001361 15111220237 0011055 0 ustar 00 <?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 = []; } } src/Concerns/Interactivity.php 0000644 00000001371 15111220237 0012450 0 ustar 00 <?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; } } src/Concerns/Fallback.php 0000644 00000002630 15111220237 0011310 0 ustar 00 <?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); } } src/Concerns/TypedValue.php 0000644 00000010471 15111220237 0011675 0 ustar 00 <?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): void { $this->typedValue = $default; if ($this->typedValue) { $this->cursorPosition = mb_strlen($this->typedValue); } $this->on('key', function ($key) use ($submit, $ignore) { 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 && $submit) { $this->submit(); return; } 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): string { $before = mb_substr($value, 0, $cursorPosition); $current = mb_substr($value, $cursorPosition, 1); $after = mb_substr($value, $cursorPosition + 1); $cursor = mb_strlen($current) ? $current : ' '; $spaceBefore = $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 - ($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) .$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))); } } src/Concerns/Scrolling.php 0000644 00000005644 15111220237 0011555 0 ustar 00 <?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 = 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; } } src/Concerns/Colors.php 0000644 00000007406 15111220237 0011060 0 ustar 00 <?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"; } } src/Concerns/Cursor.php 0000644 00000002270 15111220237 0011066 0 ustar 00 <?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); } } src/Concerns/Termwind.php 0000644 00000001017 15111220237 0011400 0 ustar 00 <?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); } } src/Concerns/Truncation.php 0000644 00000000766 15111220237 0011747 0 ustar 00 <?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).'…'); } } src/Concerns/Erase.php 0000644 00000001143 15111220237 0010646 0 ustar 00 <?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"); } } src/ConfirmPrompt.php 0000644 00000002511 15111220237 0010634 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 ?Closure $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; } } src/TextPrompt.php 0000644 00000001514 15111220237 0010165 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 ?Closure $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); } } src/SuggestPrompt.php 0000644 00000007457 15111220237 0010676 0 ustar 00 <?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> */ 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> $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 ?Closure $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) { return $this->matches = array_values(($this->options)($this->value())); } 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]; } } src/Progress.php 0000644 00000010656 15111220237 0007652 0 ustar 00 <?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) { 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(); } } src/Terminal.php 0000644 00000004226 15111220240 0007607 0 ustar 00 <?php namespace Laravel\Prompts; use RuntimeException; use Symfony\Component\Console\Terminal as SymfonyTerminal; class Terminal { /** * The initial TTY mode. */ protected ?string $initialTtyMode; /** * The number of columns in the terminal. */ protected int $cols; /** * The number of lines in the terminal. */ protected int $lines; /** * 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->cols ??= (new SymfonyTerminal())->getWidth(); } /** * Get the number of lines in the terminal. */ public function lines(): int { return $this->lines ??= (new SymfonyTerminal())->getHeight(); } /** * 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; } } src/MultiSelectPrompt.php 0000644 00000007336 15111220240 0011475 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 ?Closure $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; } } } src/Key.php 0000644 00000002503 15111220240 0006560 0 ustar 00 <?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"; /** * End */ const CTRL_E = "\x05"; /** * 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; } } src/Themes/Contracts/Scrolling.php 0000644 00000000314 15111220240 0013147 0 ustar 00 <?php namespace Laravel\Prompts\Themes\Contracts; interface Scrolling { /** * The number of lines to reserve outside of the scrollable area. */ public function reservedLines(): int; } src/Themes/Default/PasswordPromptRenderer.php 0000644 00000003275 15111220240 0015343 0 ustar 00 <?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('Cancelled.'), '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 ), }; } } src/Themes/Default/MultiSearchPromptRenderer.php 0000644 00000013670 15111220240 0015761 0 ustar 00 <?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('Cancelled'), '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() - 10)) ->map(function ($label, $key) use ($prompt) { $index = array_search($key, array_keys($prompt->matches())); $active = $index === $prompt->highlighted; $selected = array_is_list($prompt->visible()) ? 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(array_is_list($prompt->matches()) ? $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; } } src/Themes/Default/SearchPromptRenderer.php 0000644 00000010642 15111220240 0014742 0 ustar 00 <?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('Cancelled'), 'error' => $this ->box( $this->truncate($prompt->label, $prompt->terminal()->cols() - 6), $prompt->valueWithCursor($maxWidth), $this->renderOptions($prompt), color: 'yellow', ) ->warning($this->truncate($prompt->error, $prompt->terminal()->cols() - 5)), 'searching' => $this ->box( $this->cyan($this->truncate($prompt->label, $prompt->terminal()->cols() - 6)), $this->valueWithCursorAndSearchIcon($prompt, $maxWidth), $this->renderOptions($prompt), ) ->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; } } src/Themes/Default/SpinnerRenderer.php 0000644 00000001646 15111220240 0013755 0 ustar 00 <?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}"); } } src/Themes/Default/NoteRenderer.php 0000644 00000002734 15111220240 0013243 0 ustar 00 <?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; } } } src/Themes/Default/Concerns/DrawsBoxes.php 0000644 00000005372 15111220240 0014503 0 ustar 00 <?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 { $text = preg_replace("/\e[^m]*m/", '', $text); return preg_replace("/<(?:(?:[fb]g|options)=[a-z,;]+)+>(.*?)<\/>/i", '$1', $text); } } src/Themes/Default/Concerns/DrawsScrollbars.php 0000644 00000003005 15111220240 0015520 0 ustar 00 <?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; } } src/Themes/Default/TableRenderer.php 0000644 00000002466 15111220240 0013367 0 ustar 00 <?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; } } src/Themes/Default/MultiSelectPromptRenderer.php 0000644 00000010723 15111220240 0015767 0 ustar 00 <?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('Cancelled.'), '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; } } src/Themes/Default/ConfirmPromptRenderer.php 0000644 00000004560 15111220240 0015134 0 ustar 00 <?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('Cancelled.'), '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}"; } } src/Themes/Default/SelectPromptRenderer.php 0000644 00000006234 15111220240 0014756 0 ustar 00 <?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('Cancelled.'), '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; } } src/Themes/Default/Renderer.php 0000644 00000005176 15111220240 0012420 0 ustar 00 <?php namespace Laravel\Prompts\Themes\Default; use Laravel\Prompts\Concerns\Colors; use Laravel\Prompts\Concerns\Truncation; use Laravel\Prompts\Prompt; use RuntimeException; 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) { $this->checkTerminalSize($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 : ''); } /** * Check that the terminal is large enough to render the prompt. */ private function checkTerminalSize(Prompt $prompt): void { $required = 8; $actual = $prompt->terminal()->lines(); if ($actual < $required) { throw new RuntimeException( "The terminal height must be at least [$required] lines but is currently [$actual]. Please increase the height or reduce the font size." ); } } } src/Themes/Default/ProgressRenderer.php 0000644 00000004021 15111220240 0014131 0 ustar 00 <?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('Cancelled.'), 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 ) }; } } src/Themes/Default/SuggestPromptRenderer.php 0000644 00000010067 15111220240 0015157 0 ustar 00 <?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('Cancelled'), '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; } } src/Themes/Default/TextPromptRenderer.php 0000644 00000003316 15111220240 0014461 0 ustar 00 <?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('Cancelled.'), '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 ) }; } } src/error_log 0000644 00000010442 15111220240 0007235 0 ustar 00 [20-Nov-2025 06:25:39 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php:7 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php on line 7 [20-Nov-2025 06:28:29 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php on line 8 [20-Nov-2025 11:17:26 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php:7 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php on line 7 [20-Nov-2025 11:18:14 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php on line 8 [20-Nov-2025 14:48:26 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSearchPrompt.php:7 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSearchPrompt.php on line 7 [20-Nov-2025 14:55:01 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSelectPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSelectPrompt.php on line 8 [20-Nov-2025 15:47:17 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SearchPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SearchPrompt.php on line 8 [20-Nov-2025 15:47:20 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SelectPrompt.php:9 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SelectPrompt.php on line 9 [20-Nov-2025 17:12:47 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSearchPrompt.php:7 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSearchPrompt.php on line 7 [20-Nov-2025 17:15:35 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSelectPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/MultiSelectPrompt.php on line 8 [25-Nov-2025 02:32:40 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php:7 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/ConfirmPrompt.php on line 7 [25-Nov-2025 02:33:34 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php:8 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SuggestPrompt.php on line 8 [25-Nov-2025 03:01:54 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/Progress.php:13 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/Progress.php on line 13 [25-Nov-2025 03:04:19 UTC] PHP Fatal error: Uncaught Error: Class "Laravel\Prompts\Prompt" not found in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SelectPrompt.php:9 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/laravel/prompts/src/SelectPrompt.php on line 9 src/Prompt.php 0000644 00000021333 15111220240 0007313 0 ustar 00 <?php namespace Laravel\Prompts; use Closure; 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 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. */ protected ?Closure $validate; /** * Indicates if the prompt has been validated. */ protected bool $validated = false; /** * 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) { static::terminal()->exit(); } return $this->value(); } } } finally { $this->clearListeners(); } } /** * 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(); } /** * Render the prompt. */ protected function render(): void { $frame = $this->renderTheme(); if ($frame === $this->prevFrame) { return; } if ($this->state === 'initial') { static::output()->write($frame); $this->state = 'active'; $this->prevFrame = $frame; return; } $this->resetCursorPosition(); // Ensure that the full frame is buffered so subsequent output can see how many trailing newlines were written. if ($this->state === 'submit') { $this->eraseDown(); static::output()->write($frame); $this->prevFrame = ''; return; } $diff = $this->diffLines($this->prevFrame, $frame); if (count($diff) === 1) { // Update the single line that changed. $diffLine = $diff[0]; $this->moveCursor(0, $diffLine); $this->eraseLines(1); $lines = explode(PHP_EOL, $frame); static::output()->write($lines[$diffLine]); $this->moveCursor(0, count($lines) - $diffLine - 1); } elseif (count($diff) > 1) { // Re-render everything past the first change $diffLine = $diff[0]; $this->moveCursor(0, $diffLine); $this->eraseDown(); $lines = explode(PHP_EOL, $frame); $newLines = array_slice($lines, $diffLine); static::output()->write(implode(PHP_EOL, $newLines)); } $this->prevFrame = $frame; } /** * Submit the prompt. */ protected function submit(): void { $this->validate($this->value()); if ($this->state !== 'error') { $this->state = 'submit'; } } /** * Reset the cursor position to the beginning of the previous frame. */ private function resetCursorPosition(): void { $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; $this->moveCursor(-999, $lines * -1); } /** * Get the difference between two strings. * * @return array<int> */ private function diffLines(string $a, string $b): array { if ($a === $b) { return []; } $aLines = explode(PHP_EOL, $a); $bLines = explode(PHP_EOL, $b); $diff = []; for ($i = 0; $i < max(count($aLines), count($bLines)); $i++) { if (! isset($aLines[$i]) || ! isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { $diff[] = $i; } } return $diff; } /** * 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_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)) { return; } $error = ($this->validate)($value); 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(); } } README.md 0000644 00000003526 15111220240 0006015 0 ustar 00 <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). composer.json 0000644 00000002151 15111220240 0007251 0 ustar 00 { "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.10", "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" } LICENSE.md 0000644 00000002063 15111220240 0006135 0 ustar 00 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. phpunit.xml 0000644 00000001050 15111220240 0006735 0 ustar 00 <?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>