diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e22ac961..a3857fc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: matrix: php: [8.1, 8.2, 8.3] - name: PHP ${{ matrix.php }} + name: PHP ${{ matrix.php }} - With Collections steps: - name: Checkout code @@ -45,3 +45,43 @@ jobs: - name: Execute tests run: vendor/bin/pest + + without_collections: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: true + matrix: + php: [8.1, 8.2, 8.3] + + name: PHP ${{ matrix.php }} - Without Collections + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: none + + - name: Remove collections + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer remove illuminate/collections --dev --no-interaction --no-update + + - name: Install dependencies + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Execute tests + run: vendor/bin/pest diff --git a/composer.json b/composer.json index 040ff75f..462bf0a8 100644 --- a/composer.json +++ b/composer.json @@ -19,14 +19,15 @@ "require": { "php": "^8.1", "ext-mbstring": "*", - "illuminate/collections": "^10.0|^11.0", + "composer-runtime-api": "^2.2", "symfony/console": "^6.2|^7.0" }, "require-dev": { "phpstan/phpstan": "^1.11", "pestphp/pest": "^2.3", "mockery/mockery": "^1.5", - "phpstan/phpstan-mockery": "^1.1" + "phpstan/phpstan-mockery": "^1.1", + "illuminate/collections": "^10.0|^11.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", @@ -42,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-main": "0.2.x-dev" + "dev-main": "0.3.x-dev" } }, "prefer-stable": true, diff --git a/src/Key.php b/src/Key.php index 28d0ddc0..21b8bf22 100644 --- a/src/Key.php +++ b/src/Key.php @@ -99,6 +99,14 @@ class Key */ public static function oneOf(array $keys, string $match): ?string { - return collect($keys)->flatten()->contains($match) ? $match : null; + foreach ($keys as $key) { + if (is_array($key) && static::oneOf($key, $match) !== null) { + return $match; + } elseif ($key === $match) { + return $match; + } + } + + return null; } } diff --git a/src/MultiSearchPrompt.php b/src/MultiSearchPrompt.php index 083f56b9..25fcdfed 100644 --- a/src/MultiSearchPrompt.php +++ b/src/MultiSearchPrompt.php @@ -3,6 +3,7 @@ namespace Laravel\Prompts; use Closure; +use Laravel\Prompts\Support\Utils; class MultiSearchPrompt extends Prompt { @@ -140,7 +141,7 @@ public function visible(): array */ protected function toggleAll(): void { - $allMatchesSelected = collect($this->matches)->every(fn ($label, $key) => $this->isList() + $allMatchesSelected = Utils::allMatch($this->matches, fn ($label, $key) => $this->isList() ? array_key_exists($label, $this->values) : array_key_exists($key, $this->values)); diff --git a/src/Support/Result.php b/src/Support/Result.php index 42a30be7..092374e6 100644 --- a/src/Support/Result.php +++ b/src/Support/Result.php @@ -11,9 +11,10 @@ */ final class Result { - public function __construct( - public readonly mixed $value, - ) {} + public function __construct(public readonly mixed $value) + { + // + } public static function from(mixed $value): self { diff --git a/src/Support/Utils.php b/src/Support/Utils.php new file mode 100644 index 00000000..580f13a7 --- /dev/null +++ b/src/Support/Utils.php @@ -0,0 +1,53 @@ + $values + */ + public static function allMatch(array $values, Closure $callback): bool + { + foreach ($values as $key => $value) { + if (! $callback($value, $key)) { + return false; + } + } + + return true; + } + + /** + * Get the last item from an array or null if it doesn't exist. + * + * @param array $array + */ + public static function last(array $array): mixed + { + return array_reverse($array)[0] ?? null; + } + + /** + * Returns the key of the first element in the array that satisfies the callback. + * + * @param array $array + */ + public static function search(array $array, Closure $callback): int|string|false + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $key; + } + } + + return false; + } +} diff --git a/src/TextareaPrompt.php b/src/TextareaPrompt.php index e5a33666..eef13793 100644 --- a/src/TextareaPrompt.php +++ b/src/TextareaPrompt.php @@ -3,6 +3,7 @@ namespace Laravel\Prompts; use Closure; +use Laravel\Prompts\Support\Utils; class TextareaPrompt extends Prompt { @@ -113,10 +114,10 @@ protected function handleUpKey(): void return; } - $lines = collect($this->lines()); + $lines = $this->lines(); // Line length + 1 for the newline character - $lineLengths = $lines->map(fn ($line, $index) => mb_strlen($line) + ($index === $lines->count() - 1 ? 0 : 1)); + $lineLengths = array_map(fn ($line, $index) => mb_strlen($line) + ($index === count($lines) - 1 ? 0 : 1), $lines, range(0, count($lines) - 1)); $currentLineIndex = $this->currentLineIndex(); @@ -127,17 +128,17 @@ protected function handleUpKey(): void return; } - $currentLines = $lineLengths->slice(0, $currentLineIndex + 1); + $currentLines = array_slice($lineLengths, 0, $currentLineIndex + 1); - $currentColumn = $currentLines->last() - ($currentLines->sum() - $this->cursorPosition); + $currentColumn = Utils::last($currentLines) - (array_sum($currentLines) - $this->cursorPosition); - $destinationLineLength = ($lineLengths->get($currentLineIndex - 1) ?? $currentLines->first()) - 1; + $destinationLineLength = ($lineLengths[$currentLineIndex - 1] ?? $currentLines[0]) - 1; $newColumn = min($destinationLineLength, $currentColumn); - $fullLines = $currentLines->slice(0, -2); + $fullLines = array_slice($currentLines, 0, -2); - $this->cursorPosition = $fullLines->sum() + $newColumn; + $this->cursorPosition = array_sum($fullLines) + $newColumn; } /** @@ -145,34 +146,34 @@ protected function handleUpKey(): void */ protected function handleDownKey(): void { - $lines = collect($this->lines()); + $lines = $this->lines(); // Line length + 1 for the newline character - $lineLengths = $lines->map(fn ($line, $index) => mb_strlen($line) + ($index === $lines->count() - 1 ? 0 : 1)); + $lineLengths = array_map(fn ($line, $index) => mb_strlen($line) + ($index === count($lines) - 1 ? 0 : 1), $lines, range(0, count($lines) - 1)); $currentLineIndex = $this->currentLineIndex(); - if ($currentLineIndex === $lines->count() - 1) { + if ($currentLineIndex === count($lines) - 1) { // They're already at the last line, jump them to the last position - $this->cursorPosition = mb_strlen($lines->implode(PHP_EOL)); + $this->cursorPosition = mb_strlen(implode(PHP_EOL, $lines)); return; } // Lines up to and including the current line - $currentLines = $lineLengths->slice(0, $currentLineIndex + 1); + $currentLines = array_slice($lineLengths, 0, $currentLineIndex + 1); - $currentColumn = $currentLines->last() - ($currentLines->sum() - $this->cursorPosition); + $currentColumn = Utils::last($currentLines) - (array_sum($currentLines) - $this->cursorPosition); - $destinationLineLength = $lineLengths->get($currentLineIndex + 1) ?? $currentLines->last(); + $destinationLineLength = $lineLengths[$currentLineIndex + 1] ?? Utils::last($currentLines); - if ($currentLineIndex + 1 !== $lines->count() - 1) { + if ($currentLineIndex + 1 !== count($lines) - 1) { $destinationLineLength--; } $newColumn = min(max(0, $destinationLineLength), $currentColumn); - $this->cursorPosition = $currentLines->sum() + $newColumn; + $this->cursorPosition = array_sum($currentLines) + $newColumn; } /** @@ -207,7 +208,7 @@ protected function currentLineIndex(): int { $totalLineLength = 0; - return (int) collect($this->lines())->search(function ($line) use (&$totalLineLength) { + return (int) Utils::search($this->lines(), function ($line) use (&$totalLineLength) { $totalLineLength += mb_strlen($line) + 1; return $totalLineLength > $this->cursorPosition; diff --git a/src/Themes/Default/Concerns/DrawsBoxes.php b/src/Themes/Default/Concerns/DrawsBoxes.php index 0eaba8ce..d54054f5 100644 --- a/src/Themes/Default/Concerns/DrawsBoxes.php +++ b/src/Themes/Default/Concerns/DrawsBoxes.php @@ -24,14 +24,10 @@ protected function box( ): 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() - ); + $bodyLines = explode(PHP_EOL, $body); + $footerLines = array_filter(explode(PHP_EOL, $footer)); + + $width = $this->longest(array_merge($bodyLines, $footerLines, [$title])); $titleLength = mb_strwidth($this->stripEscapeSequences($title)); $titleLabel = $titleLength > 0 ? " {$title} " : ''; @@ -39,16 +35,16 @@ protected function box( $this->line("{$this->{$color}(' ┌')}{$titleLabel}{$this->{$color}($topBorder.'┐')}"); - $bodyLines->each(function ($line) use ($width, $color) { + foreach ($bodyLines as $line) { $this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}"); - }); + } - if ($footerLines->isNotEmpty()) { + if (count($footerLines) > 0) { $this->line($this->{$color}(' ├'.str_repeat('─', $width + 2).'┤')); - $footerLines->each(function ($line) use ($width, $color) { + foreach ($footerLines as $line) { $this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}"); - }); + } } $this->line($this->{$color}(' └'.str_repeat( diff --git a/src/Themes/Default/Concerns/DrawsScrollbars.php b/src/Themes/Default/Concerns/DrawsScrollbars.php index bb32f00c..e8906ab3 100644 --- a/src/Themes/Default/Concerns/DrawsScrollbars.php +++ b/src/Themes/Default/Concerns/DrawsScrollbars.php @@ -9,10 +9,12 @@ trait DrawsScrollbars /** * Render a scrollbar beside the visible items. * - * @param \Illuminate\Support\Collection $visible - * @return \Illuminate\Support\Collection + * @template T of array|\Illuminate\Support\Collection + * + * @param T $visible + * @return T */ - protected function scrollbar(Collection $visible, int $firstVisible, int $height, int $total, int $width, string $color = 'cyan'): Collection + protected function scrollbar(array|Collection $visible, int $firstVisible, int $height, int $total, int $width, string $color = 'cyan'): array|Collection { if ($height >= $total) { return $visible; @@ -20,13 +22,14 @@ protected function scrollbar(Collection $visible, int $firstVisible, int $height $scrollPosition = $this->scrollPosition($firstVisible, $height, $total); - return $visible // @phpstan-ignore return.type - ->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), - }); + $lines = $visible instanceof Collection ? $visible->all() : $visible; + + $result = array_map(fn ($line, $index) => match ($index) { + $scrollPosition => preg_replace('/.$/', $this->{$color}('┃'), $this->pad($line, $width)) ?? '', + default => preg_replace('/.$/', $this->gray('│'), $this->pad($line, $width)) ?? '', + }, array_values($lines), range(0, count($lines) - 1)); + + return $visible instanceof Collection ? new Collection($result) : $result; // @phpstan-ignore return.type (https://github.com/phpstan/phpstan/issues/11663) } /** diff --git a/src/Themes/Default/Concerns/InteractsWithStrings.php b/src/Themes/Default/Concerns/InteractsWithStrings.php index 25a2363f..01fe4edf 100644 --- a/src/Themes/Default/Concerns/InteractsWithStrings.php +++ b/src/Themes/Default/Concerns/InteractsWithStrings.php @@ -13,9 +13,7 @@ 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() + count($lines) > 0 ? max(array_map(fn ($line) => mb_strwidth($this->stripEscapeSequences($line)) + $padding, $lines)) : null ); } diff --git a/src/Themes/Default/MultiSearchPromptRenderer.php b/src/Themes/Default/MultiSearchPromptRenderer.php index e3f71205..5f701b0b 100644 --- a/src/Themes/Default/MultiSearchPromptRenderer.php +++ b/src/Themes/Default/MultiSearchPromptRenderer.php @@ -109,28 +109,28 @@ protected function renderOptions(MultiSearchPrompt $prompt): string return $this->gray(' '.($prompt->state === 'searching' ? 'Searching...' : 'No results.')); } - return $this->scrollbar( - collect($prompt->visible()) - ->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 12)) - ->map(function ($label, $key) use ($prompt) { - $index = array_search($key, array_keys($prompt->matches())); - $active = $index === $prompt->highlighted; - $selected = $prompt->isList() - ? in_array($label, $prompt->value()) - : in_array($key, $prompt->value()); - - return match (true) { - $active && $selected => "{$this->cyan('› ◼')} {$label} ", - $active => "{$this->cyan('›')} ◻ {$label} ", - $selected => " {$this->cyan('◼')} {$this->dim($label)} ", - default => " {$this->dim('◻')} {$this->dim($label)} ", - }; - }), + return implode(PHP_EOL, $this->scrollbar( + array_map(function ($label, $key) use ($prompt) { + $label = $this->truncate($label, $prompt->terminal()->cols() - 12); + + $index = array_search($key, array_keys($prompt->matches())); + $active = $index === $prompt->highlighted; + $selected = $prompt->isList() + ? in_array($label, $prompt->value()) + : in_array($key, $prompt->value()); + + return match (true) { + $active && $selected => "{$this->cyan('› ◼')} {$label} ", + $active => "{$this->cyan('›')} ◻ {$label} ", + $selected => " {$this->cyan('◼')} {$this->dim($label)} ", + default => " {$this->dim('◻')} {$this->dim($label)} ", + }; + }, $prompt->visible(), array_keys($prompt->visible())), $prompt->firstVisible, $prompt->scroll, count($prompt->matches()), min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6) - )->implode(PHP_EOL); + )); } /** @@ -155,9 +155,11 @@ protected function getInfoText(MultiSearchPrompt $prompt): string { $info = count($prompt->value()).' selected'; - $hiddenCount = count($prompt->value()) - collect($prompt->matches()) - ->filter(fn ($label, $key) => in_array($prompt->isList() ? $label : $key, $prompt->value())) - ->count(); + $hiddenCount = count($prompt->value()) - count(array_filter( + $prompt->matches(), + fn ($label, $key) => in_array($prompt->isList() ? $label : $key, $prompt->value()), + ARRAY_FILTER_USE_BOTH + )); if ($hiddenCount > 0) { $info .= " ($hiddenCount hidden)"; diff --git a/src/Themes/Default/MultiSelectPromptRenderer.php b/src/Themes/Default/MultiSelectPromptRenderer.php index f24b5efc..7afc1b8f 100644 --- a/src/Themes/Default/MultiSelectPromptRenderer.php +++ b/src/Themes/Default/MultiSelectPromptRenderer.php @@ -58,42 +58,41 @@ public function __invoke(MultiSelectPrompt $prompt): string */ 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()); + return implode(PHP_EOL, $this->scrollbar( + array_values(array_map(function ($label, $key) use ($prompt) { + $label = $this->truncate($label, $prompt->terminal()->cols() - 12); - 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)} ", - }); - } + $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()); - 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(), + 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)} ", + }; + }, $visible = $prompt->visible(), array_keys($visible))), $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); + )); } /** diff --git a/src/Themes/Default/NoteRenderer.php b/src/Themes/Default/NoteRenderer.php index 2787c70a..90523d82 100644 --- a/src/Themes/Default/NoteRenderer.php +++ b/src/Themes/Default/NoteRenderer.php @@ -11,44 +11,53 @@ class NoteRenderer extends Renderer */ public function __invoke(Note $note): string { - $lines = collect(explode(PHP_EOL, $note->message)); + $lines = 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 = array_map(fn ($line) => " {$line} ", $lines); + $longest = max(array_map(fn ($line) => strlen($line), $lines)); - $lines - ->each(function ($line) use ($longest) { - $line = str_pad($line, $longest, ' '); - $this->line(" {$this->bgCyan($this->black($line))}"); - }); + foreach ($lines as $line) { + $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}"))); + foreach ($lines as $line) { + $this->line($this->yellow(" {$line}")); + } return $this; case 'error': - $lines->each(fn ($line) => $this->line($this->red(" {$line}"))); + foreach ($lines as $line) { + $this->line($this->red(" {$line}")); + } return $this; case 'alert': - $lines->each(fn ($line) => $this->line(" {$this->bgRed($this->white(" {$line} "))}")); + foreach ($lines as $line) { + $this->line(" {$this->bgRed($this->white(" {$line} "))}"); + } return $this; case 'info': - $lines->each(fn ($line) => $this->line($this->green(" {$line}"))); + foreach ($lines as $line) { + $this->line($this->green(" {$line}")); + } return $this; default: - $lines->each(fn ($line) => $this->line(" {$line}")); + foreach ($lines as $line) { + $this->line(" {$line}"); + } return $this; } diff --git a/src/Themes/Default/PausePromptRenderer.php b/src/Themes/Default/PausePromptRenderer.php index 0e51081d..635905d1 100644 --- a/src/Themes/Default/PausePromptRenderer.php +++ b/src/Themes/Default/PausePromptRenderer.php @@ -13,12 +13,13 @@ class PausePromptRenderer extends Renderer */ public function __invoke(PausePrompt $prompt): string { - match ($prompt->state) { - 'submit' => collect(explode(PHP_EOL, $prompt->message)) - ->each(fn ($line) => $this->line($this->gray(" {$line}"))), - default => collect(explode(PHP_EOL, $prompt->message)) - ->each(fn ($line) => $this->line($this->green(" {$line}"))) - }; + $lines = explode(PHP_EOL, $prompt->message); + + $color = $prompt->state === 'submit' ? 'green' : 'gray'; + + foreach ($lines as $line) { + $this->line(" {$this->{$color}($line)}"); + } return $this; } diff --git a/src/Themes/Default/SearchPromptRenderer.php b/src/Themes/Default/SearchPromptRenderer.php index 7de4b634..7d93bf57 100644 --- a/src/Themes/Default/SearchPromptRenderer.php +++ b/src/Themes/Default/SearchPromptRenderer.php @@ -106,22 +106,21 @@ protected function renderOptions(SearchPrompt $prompt): string 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(), + return implode(PHP_EOL, $this->scrollbar( + array_values(array_map(function ($label, $key) use ($prompt) { + $label = $this->truncate($label, $prompt->terminal()->cols() - 10); + + $index = array_search($key, array_keys($prompt->matches())); + + return $prompt->highlighted === $index + ? "{$this->cyan('›')} {$label} " + : " {$this->dim($label)} "; + }, $visible = $prompt->visible(), array_keys($visible))), $prompt->firstVisible, $prompt->scroll, count($prompt->matches()), min($this->longest($prompt->matches(), padding: 4), $prompt->terminal()->cols() - 6) - )->implode(PHP_EOL); + )); } /** diff --git a/src/Themes/Default/SelectPromptRenderer.php b/src/Themes/Default/SelectPromptRenderer.php index 8337b934..2a9fb58e 100644 --- a/src/Themes/Default/SelectPromptRenderer.php +++ b/src/Themes/Default/SelectPromptRenderer.php @@ -58,30 +58,29 @@ public function __invoke(SelectPrompt $prompt): string */ 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)); + return implode(PHP_EOL, $this->scrollbar( + array_values(array_map(function ($label, $key) use ($prompt) { + $label = $this->truncate($label, $prompt->terminal()->cols() - 12); - if ($prompt->state === 'cancel') { - return $this->dim($prompt->highlighted === $index - ? "› ● {$this->strikethrough($label)} " - : " ○ {$this->strikethrough($label)} " - ); - } + $index = array_search($key, array_keys($prompt->options)); - return $prompt->highlighted === $index - ? "{$this->cyan('›')} {$this->cyan('●')} {$label} " - : " {$this->dim('○')} {$this->dim($label)} "; - }) - ->values(), + 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)} "; + }, $visible = $prompt->visible(), array_keys($visible))), $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); + )); } /** diff --git a/src/Themes/Default/SuggestPromptRenderer.php b/src/Themes/Default/SuggestPromptRenderer.php index 5e08f174..e679b681 100644 --- a/src/Themes/Default/SuggestPromptRenderer.php +++ b/src/Themes/Default/SuggestPromptRenderer.php @@ -97,19 +97,20 @@ protected function renderOptions(SuggestPrompt $prompt): string return ''; } - return $this->scrollbar( - collect($prompt->visible()) - ->map(fn ($label) => $this->truncate($label, $prompt->terminal()->cols() - 10)) - ->map(fn ($label, $key) => $prompt->highlighted === $key + return implode(PHP_EOL, $this->scrollbar( + array_map(function ($label, $key) use ($prompt) { + $label = $this->truncate($label, $prompt->terminal()->cols() - 12); + + return $prompt->highlighted === $key ? "{$this->cyan('›')} {$label} " - : " {$this->dim($label)} " - ), + : " {$this->dim($label)} "; + }, $visible = $prompt->visible(), array_keys($visible)), $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); + )); } /** diff --git a/src/Themes/Default/TableRenderer.php b/src/Themes/Default/TableRenderer.php index c2d17bb9..c8765a1e 100644 --- a/src/Themes/Default/TableRenderer.php +++ b/src/Themes/Default/TableRenderer.php @@ -34,8 +34,9 @@ public function __invoke(Table $table): string ->setStyle($tableStyle) ->render(); - collect(explode(PHP_EOL, trim($buffered->content(), PHP_EOL))) - ->each(fn ($line) => $this->line(' '.$line)); + foreach (explode(PHP_EOL, trim($buffered->content(), PHP_EOL)) as $line) { + $this->line(' '.$line); + } return $this; } diff --git a/src/Themes/Default/TextareaPromptRenderer.php b/src/Themes/Default/TextareaPromptRenderer.php index a639fa75..485529db 100644 --- a/src/Themes/Default/TextareaPromptRenderer.php +++ b/src/Themes/Default/TextareaPromptRenderer.php @@ -21,13 +21,13 @@ public function __invoke(TextareaPrompt $prompt): string 'submit' => $this ->box( $this->dim($this->truncate($prompt->label, $prompt->width)), - collect($prompt->lines())->implode(PHP_EOL), + implode(PHP_EOL, $prompt->lines()), ), 'cancel' => $this ->box( $this->truncate($prompt->label, $prompt->width), - collect($prompt->lines())->map(fn ($line) => $this->strikethrough($this->dim($line)))->implode(PHP_EOL), + implode(PHP_EOL, array_map(fn ($line) => $this->strikethrough($this->dim($line)), $prompt->lines())), color: 'red', ) ->error($prompt->cancelMessage), @@ -60,21 +60,21 @@ public function __invoke(TextareaPrompt $prompt): string */ protected function renderText(TextareaPrompt $prompt): string { - $visible = collect($prompt->visible()); + $visible = $prompt->visible(); - while ($visible->count() < $prompt->scroll) { - $visible->push(''); + while (count($visible) < $prompt->scroll) { + $visible[] = ''; } $longest = $this->longest($prompt->lines()) + 2; - return $this->scrollbar( + return implode(PHP_EOL, $this->scrollbar( $visible, $prompt->firstVisible, $prompt->scroll, count($prompt->lines()), min($longest, $prompt->width + 2), - )->implode(PHP_EOL); + )); } /** diff --git a/tests/Feature/ArchitectureTest.php b/tests/Feature/ArchitectureTest.php new file mode 100644 index 00000000..d7d80b9d --- /dev/null +++ b/tests/Feature/ArchitectureTest.php @@ -0,0 +1,5 @@ +expect('Laravel\Prompts') + ->not->toUse(['collect']); diff --git a/tests/Feature/MultiSearchPromptTest.php b/tests/Feature/MultiSearchPromptTest.php index b5ce5dc3..0ca4ffc7 100644 --- a/tests/Feature/MultiSearchPromptTest.php +++ b/tests/Feature/MultiSearchPromptTest.php @@ -107,25 +107,35 @@ expect($result)->toBe($expected); })->with([ 'associative' => [ - fn ($value) => collect([ - 'red' => 'Red', - 'orange' => 'Orange', - 'yellow' => 'Yellow', - 'green' => 'Green', - 'blue' => 'Blue', - 'indigo' => 'Indigo', - 'violet' => 'Violet', - ])->when( - strlen($value), - fn ($colors) => $colors->filter(fn ($label) => str_contains(strtolower($label), strtolower($value))) - )->all(), + function ($value) { + $options = [ + 'red' => 'Red', + 'orange' => 'Orange', + 'yellow' => 'Yellow', + 'green' => 'Green', + 'blue' => 'Blue', + 'indigo' => 'Indigo', + 'violet' => 'Violet', + ]; + + if (strlen($value) === 0) { + return $options; + } + + return array_filter($options, fn ($label) => str_contains(strtolower($label), strtolower($value))); + }, ['violet', 'green'], ], 'list' => [ - fn ($value) => collect(['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'])->when( - strlen($value), - fn ($colors) => $colors->filter(fn ($label) => str_contains(strtolower($label), strtolower($value))) - )->values()->all(), + function ($value) { + $options = ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet']; + + if (strlen($value) === 0) { + return $options; + } + + return array_values(array_filter($options, fn ($label) => str_contains(strtolower($label), strtolower($value)))); + }, ['Violet', 'Green'], ], ]); @@ -232,7 +242,7 @@ expect($result)->toBe($expected); })->with([ 'associative' => [ - fn ($value) => strlen($value) > 0 ? collect([ + fn ($value) => strlen($value) > 0 ? array_filter([ 'red' => 'Red', 'orange' => 'Orange', 'yellow' => 'Yellow', @@ -240,14 +250,11 @@ 'blue' => 'Blue', 'indigo' => 'Indigo', 'violet' => 'Violet', - ])->filter(fn ($label) => str_contains(strtolower($label), strtolower($value)))->all() : [], + ], fn ($label) => str_contains(strtolower($label), strtolower($value))) : [], ['violet', 'green'], ], 'list' => [ - fn ($value) => strlen($value) > 0 ? collect(['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet']) - ->filter(fn ($label) => str_contains(strtolower($label), strtolower($value))) - ->values() - ->all() : [], + fn ($value) => strlen($value) > 0 ? array_values(array_filter(['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'], fn ($label) => str_contains(strtolower($label), strtolower($value)))) : [], ['Violet', 'Green'], ], ]); diff --git a/tests/Feature/MultiSelectPromptTest.php b/tests/Feature/MultiSelectPromptTest.php index 1aa49e10..36423a6f 100644 --- a/tests/Feature/MultiSelectPromptTest.php +++ b/tests/Feature/MultiSelectPromptTest.php @@ -102,7 +102,7 @@ ); expect($result)->toBe(['Green']); -}); +})->skip(! depends_on_collection()); it('transforms values', function () { Prompt::fake([Key::DOWN, Key::SPACE, Key::DOWN, Key::SPACE, Key::ENTER]); diff --git a/tests/Feature/ProgressTest.php b/tests/Feature/ProgressTest.php index 2970c62f..1eb59da9 100644 --- a/tests/Feature/ProgressTest.php +++ b/tests/Feature/ProgressTest.php @@ -41,8 +41,8 @@ OUTPUT); })->with([ 'array' => [['Alabama', 'Alaska', 'Arizona', 'Arkansas']], - 'collection' => [collect(['Alabama', 'Alaska', 'Arizona', 'Arkansas'])], 'integer' => [4], + ...depends_on_collection() ? ['collection' => [collect(['Alabama', 'Alaska', 'Arizona', 'Arkansas'])]] : [], ]); it('renders a progress bar without a label', function () { diff --git a/tests/Feature/SelectPromptTest.php b/tests/Feature/SelectPromptTest.php index 8ea4689d..0f52c47d 100644 --- a/tests/Feature/SelectPromptTest.php +++ b/tests/Feature/SelectPromptTest.php @@ -61,11 +61,11 @@ 'Red', 'Green', 'Blue', - ]) + ]), ); expect($result)->toBe('Green'); -}); +})->skip(! depends_on_collection()); it('accepts default values when the options are labels', function () { Prompt::fake([Key::ENTER]); diff --git a/tests/Feature/SuggestPromptTest.php b/tests/Feature/SuggestPromptTest.php index 384a7a6f..da813a70 100644 --- a/tests/Feature/SuggestPromptTest.php +++ b/tests/Feature/SuggestPromptTest.php @@ -96,7 +96,7 @@ ])); expect($result)->toBe('Blue'); -}); +})->skip(! depends_on_collection()); it('accepts a callback returning a collection', function () { Prompt::fake(['b', Key::TAB, Key::ENTER]); @@ -116,7 +116,7 @@ ); expect($result)->toBe('Blue'); -}); +})->skip(! depends_on_collection()); it('transforms values', function () { Prompt::fake([Key::SPACE, 'J', 'e', 's', 's', Key::TAB, Key::ENTER]); diff --git a/tests/Feature/TableTest.php b/tests/Feature/TableTest.php index 121c6a98..ec946b03 100644 --- a/tests/Feature/TableTest.php +++ b/tests/Feature/TableTest.php @@ -39,20 +39,22 @@ ['Joe Dixon', '@_joedixon'], ], ], - 'collections' => [ - collect(['Name', 'Twitter']), - collect([ - ['Taylor Otwell', '@taylorotwell'], - ['Dries Vints', '@driesvints'], - ['James Brooks', '@jbrooksuk'], - ['Nuno Maduro', '@enunomaduro'], - ['Mior Muhammad Zaki', '@crynobone'], - ['Jess Archer', '@jessarchercodes'], - ['Guus Leeuw', '@phpguus'], - ['Tim MacDonald', '@timacdonald87'], - ['Joe Dixon', '@_joedixon'], - ]), - ], + ...depends_on_collection() ? [ + 'collections' => [ + collect(['Name', 'Twitter']), + collect([ + ['Taylor Otwell', '@taylorotwell'], + ['Dries Vints', '@driesvints'], + ['James Brooks', '@jbrooksuk'], + ['Nuno Maduro', '@enunomaduro'], + ['Mior Muhammad Zaki', '@crynobone'], + ['Jess Archer', '@jessarchercodes'], + ['Guus Leeuw', '@phpguus'], + ['Tim MacDonald', '@timacdonald87'], + ['Joe Dixon', '@_joedixon'], + ]), + ], + ] : [], ]); it('renders a table without headers', function ($rows) { @@ -87,17 +89,19 @@ ['Joe Dixon', '@_joedixon'], ], ], - 'collections' => [ - collect([ - ['Taylor Otwell', '@taylorotwell'], - ['Dries Vints', '@driesvints'], - ['James Brooks', '@jbrooksuk'], - ['Nuno Maduro', '@enunomaduro'], - ['Mior Muhammad Zaki', '@crynobone'], - ['Jess Archer', '@jessarchercodes'], - ['Guus Leeuw', '@phpguus'], - ['Tim MacDonald', '@timacdonald87'], - ['Joe Dixon', '@_joedixon'], - ]), - ], + ...depends_on_collection() ? [ + 'collections' => [ + collect([ + ['Taylor Otwell', '@taylorotwell'], + ['Dries Vints', '@driesvints'], + ['James Brooks', '@jbrooksuk'], + ['Nuno Maduro', '@enunomaduro'], + ['Mior Muhammad Zaki', '@crynobone'], + ['Jess Archer', '@jessarchercodes'], + ['Guus Leeuw', '@phpguus'], + ['Tim MacDonald', '@timacdonald87'], + ['Joe Dixon', '@_joedixon'], + ]), + ], + ] : [], ]); diff --git a/tests/Pest.php b/tests/Pest.php index 5949c617..2e4fcdde 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@