refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

View File

@@ -0,0 +1,60 @@
<?php
namespace Laravel\Prompts\Themes\Default\Concerns;
use Laravel\Prompts\Prompt;
trait DrawsBoxes
{
use InteractsWithStrings;
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 = 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} " : '';
$topBorder = str_repeat('─', $width - $titleLength + ($titleLength > 0 ? 0 : 2));
$this->line("{$this->{$color}(' ┌')}{$titleLabel}{$this->{$color}($topBorder.'┐')}");
foreach ($bodyLines as $line) {
$this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}");
}
if (count($footerLines) > 0) {
$this->line($this->{$color}(' ├'.str_repeat('─', $width + 2).'┤'));
foreach ($footerLines as $line) {
$this->line("{$this->{$color}(' │')} {$this->pad($line, $width)} {$this->{$color}('│')}");
}
}
if ($info) {
$info = $this->truncate($info, $width - 1);
}
$this->line($this->{$color}(' └'.str_repeat(
'─', $info ? ($width - mb_strwidth($this->stripEscapeSequences($info))) : ($width + 2)
).($info ? " {$info} " : '').'┘'));
return $this;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Laravel\Prompts\Themes\Default\Concerns;
use Illuminate\Support\Collection;
trait DrawsScrollbars
{
/**
* Render a scrollbar beside the visible items.
*
* @template T of array<int, string>|\Illuminate\Support\Collection<int, string>
*
* @param T $visible
* @return T
*/
protected function scrollbar(array|Collection $visible, int $firstVisible, int $height, int $total, int $width, string $color = 'cyan'): array|Collection
{
if ($height >= $total) {
return $visible;
}
$scrollPosition = $this->scrollPosition($firstVisible, $height, $total);
$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)
}
/**
* Return the position where the scrollbar "handle" should be rendered.
*/
protected function scrollPosition(int $firstVisible, int $height, int $total): int
{
if ($firstVisible === 0) {
return 0;
}
$maxPosition = $total - $height;
if ($firstVisible === $maxPosition) {
return $height - 1;
}
if ($height <= 2) {
return -1;
}
$percent = $firstVisible / $maxPosition;
return (int) round($percent * ($height - 3)) + 1;
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Laravel\Prompts\Themes\Default\Concerns;
trait InteractsWithStrings
{
/**
* Get the length of the longest line.
*
* @param array<string> $lines
*/
protected function longest(array $lines, int $padding = 0): int
{
return max(
$this->minWidth,
count($lines) > 0 ? max(array_map(fn ($line) => mb_strwidth($this->stripEscapeSequences($line)) + $padding, $lines)) : null
);
}
/**
* Pad text ignoring ANSI escape sequences.
*/
protected function pad(string $text, int $length, string $char = ' '): string
{
$rightPadding = str_repeat($char, max(0, $length - mb_strwidth($this->stripEscapeSequences($text))));
return "{$text}{$rightPadding}";
}
/**
* Strip ANSI escape sequences from the given text.
*/
protected function stripEscapeSequences(string $text): string
{
// Strip ANSI escape sequences.
$text = preg_replace("/\e[^m]*m/", '', $text);
// Strip Symfony named style tags.
$text = preg_replace("/<(info|comment|question|error)>(.*?)<\/\\1>/", '$2', $text);
// Strip Symfony inline style tags.
return preg_replace("/<(?:(?:[fb]g|options)=[a-z,;]+)+>(.*?)<\/>/i", '$1', $text);
}
/**
* Multi-byte version of wordwrap.
*
* @param non-empty-string $break
*/
protected function mbWordwrap(
string $string,
int $width = 75,
string $break = "\n",
bool $cut_long_words = false
): string {
$lines = explode($break, $string);
$result = [];
foreach ($lines as $originalLine) {
if (mb_strwidth($originalLine) <= $width) {
$result[] = $originalLine;
continue;
}
$words = explode(' ', $originalLine);
$line = null;
$lineWidth = 0;
if ($cut_long_words) {
foreach ($words as $index => $word) {
$characters = mb_str_split($word);
$strings = [];
$str = '';
foreach ($characters as $character) {
$tmp = $str.$character;
if (mb_strwidth($tmp) > $width) {
$strings[] = $str;
$str = $character;
} else {
$str = $tmp;
}
}
if ($str !== '') {
$strings[] = $str;
}
$words[$index] = implode(' ', $strings);
}
$words = explode(' ', implode(' ', $words));
}
foreach ($words as $word) {
$tmp = ($line === null) ? $word : $line.' '.$word;
// Look for zero-width joiner characters (combined emojis)
preg_match('/\p{Cf}/u', $word, $joinerMatches);
$wordWidth = count($joinerMatches) > 0 ? 2 : mb_strwidth($word);
$lineWidth += $wordWidth;
if ($line !== null) {
// Space between words
$lineWidth += 1;
}
if ($lineWidth <= $width) {
$line = $tmp;
} else {
$result[] = $line;
$line = $word;
$lineWidth = $wordWidth;
}
}
if ($line !== '') {
$result[] = $line;
}
$line = null;
}
return implode($break, $result);
}
/**
* Word wrap text while preserving ANSI escape sequences.
*
* @return array<int, string>
*/
protected function ansiWordwrap(string $text, int $width): array
{
// Parse segments and build character array with codes
$segments = $this->parseAnsiText($text);
$plainText = $this->stripEscapeSequences($text);
$chars = [];
foreach ($segments as $segment) {
$segmentChars = mb_str_split($segment['text']);
foreach ($segmentChars as $char) {
$chars[] = ['char' => $char, 'codes' => $segment['codes']];
}
}
// Word wrap the plain text
$wrappedLines = $this->mbWordwrap($plainText, $width, "\n", false);
$plainLines = explode("\n", $wrappedLines);
// Rebuild each wrapped line with ANSI codes
$result = [];
$charIndex = 0;
foreach ($plainLines as $plainLine) {
$line = '';
$lastCodes = '';
$lineChars = mb_str_split($plainLine);
foreach ($lineChars as $lineChar) {
// Find matching character in original (handling spaces removed by wordwrap)
while ($charIndex < count($chars) && $chars[$charIndex]['char'] !== $lineChar) {
// Skip spaces that wordwrap removed
if ($chars[$charIndex]['char'] === ' ') {
$charIndex++;
} else {
break;
}
}
if ($charIndex < count($chars)) {
$codes = $chars[$charIndex]['codes'];
if ($codes !== $lastCodes) {
if ($lastCodes !== '') {
$line .= "\e[0m";
}
if ($codes !== '') {
$line .= $codes;
}
$lastCodes = $codes;
}
$line .= $lineChar;
$charIndex++;
} else {
$line .= $lineChar;
}
}
// Close any open ANSI codes
if ($lastCodes !== '' && ! str_ends_with($line, "\e[0m")) {
$line .= "\e[0m";
}
$result[] = $line;
}
return $result;
}
/**
* Parse text into segments with their associated ANSI codes.
*
* @return array<int, array{text: string, codes: string}>
*/
protected function parseAnsiText(string $text): array
{
$segments = [];
$currentCodes = '';
$currentText = '';
$i = 0;
$textLength = strlen($text);
while ($i < $textLength) {
if ($text[$i] === "\e" && ($i + 1 < $textLength) && $text[$i + 1] === '[') {
// Save current segment if it has text
if ($currentText !== '') {
$segments[] = ['text' => $currentText, 'codes' => $currentCodes];
$currentText = '';
}
// Extract ANSI escape sequence
$escapeSequence = '';
while ($i < $textLength) {
$escapeSequence .= $text[$i];
$i++;
if (preg_match('/^\\e\\[[0-9;]*m$/', $escapeSequence)) {
// Update current codes
if ($escapeSequence === "\e[0m") {
$currentCodes = '';
} else {
$currentCodes = $escapeSequence;
}
break;
}
}
continue;
}
$currentText .= $text[$i];
$i++;
}
// Add final segment
if ($currentText !== '') {
$segments[] = ['text' => $currentText, 'codes' => $currentCodes];
}
return $segments;
}
}