From c54e099c478ddd4b571a2025370a9dcbaf1693d7 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Thu, 21 Sep 2023 19:56:27 -0400 Subject: [PATCH 01/17] playing around with streaming content from spinner --- src/Connection.php | 134 +++++++++++++++++++++++++ src/Spinner.php | 20 +++- src/SpinnerMessenger.php | 15 +++ src/Themes/Default/SpinnerRenderer.php | 6 ++ 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/Connection.php create mode 100644 src/SpinnerMessenger.php diff --git a/src/Connection.php b/src/Connection.php new file mode 100644 index 00000000..95e9ad39 --- /dev/null +++ b/src/Connection.php @@ -0,0 +1,134 @@ +socket); + + $this->timeoutSeconds = floor($this->timeout); + + $this->timeoutMicroseconds = ($this->timeout * 1_000_000) - ($this->timeoutSeconds * 1_000_000); + } + + /** + * @return self[] + */ + public static function createPair(): array + { + socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets); + + [$socketToParent, $socketToChild] = $sockets; + + return [ + new self($socketToParent), + new self($socketToChild), + ]; + } + + public function close(): self + { + socket_close($this->socket); + + return $this; + } + + public function write(string $payload): self + { + socket_set_nonblock($this->socket); + + while ($payload !== '') { + $write = [$this->socket]; + + $read = null; + + $except = null; + + try { + $selectResult = socket_select($read, $write, $except, $this->timeoutSeconds, $this->timeoutMicroseconds); + } catch (ErrorException $e) { + if ($this->isInterruptionErrorException()) { + continue; + } + + throw $e; + } + + if ($selectResult === false) { + break; + } + + if ($selectResult <= 0) { + break; + } + + $length = strlen($payload); + + $amountOfBytesSent = socket_write($this->socket, $payload, $length); + + if ($amountOfBytesSent === false || $amountOfBytesSent === $length) { + break; + } + + $payload = substr($payload, $amountOfBytesSent); + } + + return $this; + } + + public function read(): Generator + { + socket_set_nonblock($this->socket); + + while (true) { + $read = [$this->socket]; + + $write = null; + + $except = null; + + try { + $selectResult = socket_select($read, $write, $except, $this->timeoutSeconds, $this->timeoutMicroseconds); + } catch (ErrorException $e) { + if ($this->isInterruptionErrorException()) { + continue; + } + + throw $e; + } + + if ($selectResult === false) { + break; + } + + if ($selectResult <= 0) { + break; + } + + $outputFromSocket = socket_read($this->socket, $this->bufferSize); + + if ($outputFromSocket === false || $outputFromSocket === '') { + break; + } + + yield $outputFromSocket; + } + } + + private function isInterruptionErrorException(): bool + { + return 4 === socket_last_error(); + } +} diff --git a/src/Spinner.php b/src/Spinner.php index 7e1f73f0..2b59e016 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -17,11 +17,17 @@ class Spinner extends Prompt */ public int $count = 0; + public array $socketResults = []; + /** * Whether the spinner can only be rendered once. */ public bool $static = false; + protected Connection $socketToSpinner; + + protected Connection $socketToTask; + /** * Create a new Spinner instance. */ @@ -42,9 +48,12 @@ public function spin(Closure $callback): mixed { $this->capturePreviousNewLines(); + // Create a pair of socket connections so the two tasks can communicate + [$this->socketToTask, $this->socketToSpinner] = Connection::createPair(); + register_shutdown_function(fn () => $this->restoreCursor()); - if (! function_exists('pcntl_fork')) { + if (!function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -60,6 +69,10 @@ public function spin(Closure $callback): mixed if ($pid === 0) { while (true) { // @phpstan-ignore-line + foreach ($this->socketToTask->read() as $output) { + $this->socketResults[] = $output; + } + $this->render(); $this->count++; @@ -69,7 +82,7 @@ public function spin(Closure $callback): mixed } else { register_shutdown_function(fn () => posix_kill($pid, SIGHUP)); - $result = $callback(); + $result = $callback(new SpinnerMessenger($this->socketToSpinner)); posix_kill($pid, SIGHUP); @@ -92,6 +105,9 @@ protected function resetTerminal(bool $originalAsync): void pcntl_async_signals($originalAsync); pcntl_signal(SIGINT, SIG_DFL); + $this->socketToSpinner->close(); + $this->socketToTask->close(); + $this->eraseRenderedLines(); $this->showCursor(); } diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php new file mode 100644 index 00000000..0ac65189 --- /dev/null +++ b/src/SpinnerMessenger.php @@ -0,0 +1,15 @@ +socket->write($message); + } +} diff --git a/src/Themes/Default/SpinnerRenderer.php b/src/Themes/Default/SpinnerRenderer.php index c68aef4f..2dc73b75 100644 --- a/src/Themes/Default/SpinnerRenderer.php +++ b/src/Themes/Default/SpinnerRenderer.php @@ -36,6 +36,12 @@ public function __invoke(Spinner $spinner): string $frame = $this->frames[$spinner->count % count($this->frames)]; + collect($spinner->socketResults)->each(fn ($result) => $this->line(' ' . $result)); + + if (count($spinner->socketResults)) { + $this->line(''); + } + return $this->line(" {$this->cyan($frame)} {$spinner->message}"); } } From 375c3a4965617303286b2f14e43a75978cc816f7 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Fri, 22 Sep 2023 21:51:29 -0400 Subject: [PATCH 02/17] v1, largely working, but still needs work --- playground/streaming-spinner.php | 44 +++++++++++++++ src/Connection.php | 4 +- src/Output/ConsoleOutput.php | 8 +++ src/Prompt.php | 11 ++++ src/Spinner.php | 64 ++++++++++++++++----- src/SpinnerMessenger.php | 14 ++++- src/SpinnerSockets.php | 78 ++++++++++++++++++++++++++ src/Themes/Default/SpinnerRenderer.php | 6 -- 8 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 playground/streaming-spinner.php create mode 100644 src/SpinnerSockets.php diff --git a/playground/streaming-spinner.php b/playground/streaming-spinner.php new file mode 100644 index 00000000..3def90ce --- /dev/null +++ b/playground/streaming-spinner.php @@ -0,0 +1,44 @@ +start(); + + foreach ($process as $type => $data) { + if ($process::ERR === $type) { + $messenger->output($data); + } + } + + return 'Callback return'; + }, + 'Updating Composer...', +); + +spin( + function (SpinnerMessenger $messenger) { + foreach (range(1, 50) as $i) { + $messenger->line("✔︎ Step {$i}"); + + usleep(rand(50_000, 250_000)); + + if ($i === 20) { + $messenger->message('Almost there...'); + } + + if ($i === 35) { + $messenger->message('Still going...'); + } + } + }, + 'Taking necessary steps...', +); diff --git a/src/Connection.php b/src/Connection.php index 95e9ad39..8f85e573 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -18,9 +18,9 @@ protected function __construct( ) { socket_set_nonblock($this->socket); - $this->timeoutSeconds = floor($this->timeout); + $this->timeoutSeconds = (int) floor($this->timeout); - $this->timeoutMicroseconds = ($this->timeout * 1_000_000) - ($this->timeoutSeconds * 1_000_000); + $this->timeoutMicroseconds = (int) (($this->timeout * 1_000_000) - ($this->timeoutSeconds * 1_000_000)); } /** diff --git a/src/Output/ConsoleOutput.php b/src/Output/ConsoleOutput.php index 60381d62..817084db 100644 --- a/src/Output/ConsoleOutput.php +++ b/src/Output/ConsoleOutput.php @@ -46,4 +46,12 @@ public function writeDirectly(string $message): void { parent::doWrite($message, false); } + + /** + * Write output directly, bypassing newline capture. + */ + public function writeDirectlyWithFormatting(string $message): void + { + $this->writeDirectly($this->getFormatter()->format($message)); + } } diff --git a/src/Prompt.php b/src/Prompt.php index b63f4a08..a3375d50 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -169,6 +169,17 @@ protected static function writeDirectly(string $message): void }; } + /** + * Write output directly with formatting, bypassing newline capture. + */ + protected static function writeDirectlyWithFormatting(string $message): void + { + match (true) { + method_exists(static::output(), 'writeDirectlyWithFormatting') => static::output()->writeDirectlyWithFormatting($message), + default => static::writeDirectly($message), + }; + } + /** * Get the terminal instance. */ diff --git a/src/Spinner.php b/src/Spinner.php index 2b59e016..21fae285 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -3,10 +3,13 @@ namespace Laravel\Prompts; use Closure; +use Laravel\Prompts\Concerns\Colors; use RuntimeException; class Spinner extends Prompt { + use Colors; + /** * How long to wait between rendering each frame. */ @@ -17,16 +20,20 @@ class Spinner extends Prompt */ public int $count = 0; - public array $socketResults = []; + /** + * Whether the spinner has streamed output. + */ + public bool $hasStreamingOutput = false; /** * Whether the spinner can only be rendered once. */ public bool $static = false; - protected Connection $socketToSpinner; - - protected Connection $socketToTask; + /** + * The sockets used to communicate between the spinner and the task. + */ + protected SpinnerSockets $sockets; /** * Create a new Spinner instance. @@ -48,8 +55,7 @@ public function spin(Closure $callback): mixed { $this->capturePreviousNewLines(); - // Create a pair of socket connections so the two tasks can communicate - [$this->socketToTask, $this->socketToSpinner] = Connection::createPair(); + $this->sockets = SpinnerSockets::create(); register_shutdown_function(fn () => $this->restoreCursor()); @@ -69,10 +75,8 @@ public function spin(Closure $callback): mixed if ($pid === 0) { while (true) { // @phpstan-ignore-line - foreach ($this->socketToTask->read() as $output) { - $this->socketResults[] = $output; - } - + $this->setNewMessage(); + $this->renderStreamedOutput(); $this->render(); $this->count++; @@ -82,7 +86,7 @@ public function spin(Closure $callback): mixed } else { register_shutdown_function(fn () => posix_kill($pid, SIGHUP)); - $result = $callback(new SpinnerMessenger($this->socketToSpinner)); + $result = $callback($this->sockets->messenger()); posix_kill($pid, SIGHUP); @@ -97,6 +101,41 @@ public function spin(Closure $callback): mixed } } + /** + * Render any streaming output from the spinner, if available. + */ + protected function renderStreamedOutput(): void + { + $output = $this->sockets->streamingOutput(); + + if ($output !== '') { + $this->hasStreamingOutput = true; + + $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; + + $this->moveCursor(-999, $lines * -1); + $this->eraseDown(); + + collect(explode(PHP_EOL, rtrim($output))) + ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + + static::writeDirectlyWithFormatting($this->dim(str_repeat('─', 60))); + $this->writeDirectly($this->prevFrame); + } + } + + /** + * Set the new message if one is available. + */ + protected function setNewMessage(): void + { + $message = $this->sockets->message(); + + if ($message !== '') { + $this->message = $message; + } + } + /** * Reset the terminal. */ @@ -105,8 +144,7 @@ protected function resetTerminal(bool $originalAsync): void pcntl_async_signals($originalAsync); pcntl_signal(SIGINT, SIG_DFL); - $this->socketToSpinner->close(); - $this->socketToTask->close(); + $this->sockets->close(); $this->eraseRenderedLines(); $this->showCursor(); diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index 0ac65189..02e17f3a 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -4,12 +4,22 @@ class SpinnerMessenger { - public function __construct(protected Connection $socket) + public function __construct(protected Connection $socket, protected Connection $labelSocket) { } - public function send(string $message): void + public function output(string $message): void { $this->socket->write($message); } + + public function line(string $message): void + { + $this->output($message . PHP_EOL); + } + + public function message(string $message): void + { + $this->labelSocket->write($message); + } } diff --git a/src/SpinnerSockets.php b/src/SpinnerSockets.php new file mode 100644 index 00000000..640ef7d1 --- /dev/null +++ b/src/SpinnerSockets.php @@ -0,0 +1,78 @@ +outputToSpinner, $this->messageToSpinner); + } + + /** + * Get the streaming output from the spinner. + */ + public function streamingOutput(): string + { + $output = ''; + + foreach ($this->outputToTask->read() as $chunk) { + $output .= $chunk; + } + + return $output; + } + + /** + * Get the most recent message from the spinner. + */ + public function message(): string + { + $message = ''; + + foreach ($this->messageToTask->read() as $chunk) { + $message .= $chunk; + } + + return $message; + } + + /** + * Close the sockets. + */ + public function close(): void + { + $this->outputToSpinner->close(); + $this->outputToTask->close(); + $this->messageToSpinner->close(); + $this->messageToTask->close(); + } +} diff --git a/src/Themes/Default/SpinnerRenderer.php b/src/Themes/Default/SpinnerRenderer.php index 2dc73b75..c68aef4f 100644 --- a/src/Themes/Default/SpinnerRenderer.php +++ b/src/Themes/Default/SpinnerRenderer.php @@ -36,12 +36,6 @@ public function __invoke(Spinner $spinner): string $frame = $this->frames[$spinner->count % count($this->frames)]; - collect($spinner->socketResults)->each(fn ($result) => $this->line(' ' . $result)); - - if (count($spinner->socketResults)) { - $this->line(''); - } - return $this->line(" {$this->cyan($frame)} {$spinner->message}"); } } From d76d9e0ecff7155de264a7c00b8cf0c55d8839e2 Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 23 Sep 2023 01:52:04 +0000 Subject: [PATCH 03/17] Fix code styling --- playground/streaming-spinner.php | 3 +-- src/Connection.php | 3 ++- src/Spinner.php | 4 ++-- src/SpinnerMessenger.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/playground/streaming-spinner.php b/playground/streaming-spinner.php index 3def90ce..7ca02390 100644 --- a/playground/streaming-spinner.php +++ b/playground/streaming-spinner.php @@ -4,9 +4,8 @@ use Symfony\Component\Process\Process; use function Laravel\Prompts\spin; -use function Laravel\Prompts\text; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__.'/../vendor/autoload.php'; spin( function (SpinnerMessenger $messenger) { diff --git a/src/Connection.php b/src/Connection.php index 8f85e573..48ad840a 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -9,6 +9,7 @@ class Connection { protected int $timeoutSeconds; + protected int $timeoutMicroseconds; protected function __construct( @@ -129,6 +130,6 @@ public function read(): Generator private function isInterruptionErrorException(): bool { - return 4 === socket_last_error(); + return socket_last_error() === 4; } } diff --git a/src/Spinner.php b/src/Spinner.php index 21fae285..23824261 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -59,7 +59,7 @@ public function spin(Closure $callback): mixed register_shutdown_function(fn () => $this->restoreCursor()); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -117,7 +117,7 @@ protected function renderStreamedOutput(): void $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); static::writeDirectlyWithFormatting($this->dim(str_repeat('─', 60))); $this->writeDirectly($this->prevFrame); diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index 02e17f3a..5066295d 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -15,7 +15,7 @@ public function output(string $message): void public function line(string $message): void { - $this->output($message . PHP_EOL); + $this->output($message.PHP_EOL); } public function message(string $message): void From d3aa7e9187b6cf56316d0538c49107c6d487346b Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 23 Sep 2023 01:57:23 +0000 Subject: [PATCH 04/17] Fix code styling --- src/Spinner.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index 30569fd5..8efb77a1 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -62,7 +62,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -116,7 +116,7 @@ protected function renderStreamedOutput(): void $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); static::writeDirectlyWithFormatting($this->dim(str_repeat('─', 60))); $this->writeDirectly($this->prevFrame); @@ -205,7 +205,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (!empty($this->pid)) { + if (! empty($this->pid)) { posix_kill($this->pid, SIGHUP); } From fe5e08b7e0694790fe879fc0332c9b3b999a3e44 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 23 Sep 2023 13:13:17 -0400 Subject: [PATCH 05/17] removed unused flag --- src/Spinner.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index 8efb77a1..d41ccbfe 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -20,11 +20,6 @@ class Spinner extends Prompt */ public int $count = 0; - /** - * Whether the spinner has streamed output. - */ - public bool $hasStreamingOutput = false; - /** * Whether the spinner can only be rendered once. */ @@ -108,8 +103,6 @@ protected function renderStreamedOutput(): void $output = $this->sockets->streamingOutput(); if ($output !== '') { - $this->hasStreamingOutput = true; - $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; $this->moveCursor(-999, $lines * -1); From 5f153df3369027e8bb2595b976b7e98e237d8413 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 23 Sep 2023 14:40:23 -0400 Subject: [PATCH 06/17] changed visibility of resetCursorPosition --- composer.json | 3 ++- src/Prompt.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index f5f0e4a8..03ed43a9 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "phpstan/phpstan": "^1.10", "pestphp/pest": "^2.3", "mockery/mockery": "^1.5", - "phpstan/phpstan-mockery": "^1.1" + "phpstan/phpstan-mockery": "^1.1", + "spatie/ray": "^1.39" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", diff --git a/src/Prompt.php b/src/Prompt.php index eb525dba..0113f16e 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -252,7 +252,7 @@ protected function submit(): void /** * Reset the cursor position to the beginning of the previous frame. */ - private function resetCursorPosition(): void + protected function resetCursorPosition(): void { $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; From 34fb0351677f75a597aa0124ed0a48a5560b7c35 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 23 Sep 2023 14:42:31 -0400 Subject: [PATCH 07/17] closer, but still not completely accurate spacing --- src/Spinner.php | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index d41ccbfe..14e0b61f 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -35,6 +35,11 @@ class Spinner extends Prompt */ protected int $pid; + /** + * Whether the spinner has streamed output. + */ + protected bool $hasStreamingOutput = false; + /** * Create a new Spinner instance. */ @@ -57,7 +62,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (! function_exists('pcntl_fork')) { + if (!function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -84,6 +89,9 @@ public function spin(Closure $callback): mixed } else { $result = $callback($this->sockets->messenger()); + // Let the spinner finish its last cycle before exiting + usleep($this->interval * 1000); + $this->resetTerminal($originalAsync); return $result; @@ -103,16 +111,30 @@ protected function renderStreamedOutput(): void $output = $this->sockets->streamingOutput(); if ($output !== '') { - $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; + $this->resetCursorPosition(); + + $breaksAfterLine = max($this->newLinesWritten() - 1, 0); + + if ($this->hasStreamingOutput) { + $this->moveCursor(-999, -2 - $breaksAfterLine); + } - $this->moveCursor(-999, $lines * -1); $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); + ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + + $this->writeDirectly(str_repeat(PHP_EOL, max(2 - $this->newLinesWritten(), 1))); + // TODO: Calculate the width of this line based on the terminal width/boxes + static::writeDirectlyWithFormatting(' ' . $this->dim(str_repeat('─', 63)) . PHP_EOL); + + if ($breaksAfterLine > 0) { + $this->writeDirectly(str_repeat(PHP_EOL, $breaksAfterLine)); + } - static::writeDirectlyWithFormatting($this->dim(str_repeat('─', 60))); $this->writeDirectly($this->prevFrame); + + $this->hasStreamingOutput = true; } } @@ -198,7 +220,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (! empty($this->pid)) { + if (!empty($this->pid)) { posix_kill($this->pid, SIGHUP); } From f4b990855d6805c1fbe5c28f5f37db83a8c45856 Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 23 Sep 2023 18:43:01 +0000 Subject: [PATCH 08/17] Fix code styling --- src/Spinner.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index 14e0b61f..930c9b7f 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -62,7 +62,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -122,11 +122,11 @@ protected function renderStreamedOutput(): void $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); $this->writeDirectly(str_repeat(PHP_EOL, max(2 - $this->newLinesWritten(), 1))); // TODO: Calculate the width of this line based on the terminal width/boxes - static::writeDirectlyWithFormatting(' ' . $this->dim(str_repeat('─', 63)) . PHP_EOL); + static::writeDirectlyWithFormatting(' '.$this->dim(str_repeat('─', 63)).PHP_EOL); if ($breaksAfterLine > 0) { $this->writeDirectly(str_repeat(PHP_EOL, $breaksAfterLine)); @@ -220,7 +220,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (!empty($this->pid)) { + if (! empty($this->pid)) { posix_kill($this->pid, SIGHUP); } From 1fb8157748430bf7c638ff864510ae64248453c9 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 23 Sep 2023 15:00:07 -0400 Subject: [PATCH 09/17] ok simplified and closer to consistency --- src/Spinner.php | 24 +++--------------------- src/Themes/Default/SpinnerRenderer.php | 4 ++++ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index 14e0b61f..457663ac 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -3,13 +3,10 @@ namespace Laravel\Prompts; use Closure; -use Laravel\Prompts\Concerns\Colors; use RuntimeException; class Spinner extends Prompt { - use Colors; - /** * How long to wait between rendering each frame. */ @@ -38,7 +35,7 @@ class Spinner extends Prompt /** * Whether the spinner has streamed output. */ - protected bool $hasStreamingOutput = false; + public bool $hasStreamingOutput = false; /** * Create a new Spinner instance. @@ -111,30 +108,15 @@ protected function renderStreamedOutput(): void $output = $this->sockets->streamingOutput(); if ($output !== '') { - $this->resetCursorPosition(); - - $breaksAfterLine = max($this->newLinesWritten() - 1, 0); - - if ($this->hasStreamingOutput) { - $this->moveCursor(-999, -2 - $breaksAfterLine); - } + $this->hasStreamingOutput = true; + $this->resetCursorPosition(); $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); - $this->writeDirectly(str_repeat(PHP_EOL, max(2 - $this->newLinesWritten(), 1))); - // TODO: Calculate the width of this line based on the terminal width/boxes - static::writeDirectlyWithFormatting(' ' . $this->dim(str_repeat('─', 63)) . PHP_EOL); - - if ($breaksAfterLine > 0) { - $this->writeDirectly(str_repeat(PHP_EOL, $breaksAfterLine)); - } - $this->writeDirectly($this->prevFrame); - - $this->hasStreamingOutput = true; } } diff --git a/src/Themes/Default/SpinnerRenderer.php b/src/Themes/Default/SpinnerRenderer.php index c68aef4f..8a5d49d8 100644 --- a/src/Themes/Default/SpinnerRenderer.php +++ b/src/Themes/Default/SpinnerRenderer.php @@ -34,6 +34,10 @@ public function __invoke(Spinner $spinner): string $spinner->interval = $this->interval; + if ($spinner->hasStreamingOutput) { + $this->line($this->dim(str_repeat('─', 63))); + } + $frame = $this->frames[$spinner->count % count($this->frames)]; return $this->line(" {$this->cyan($frame)} {$spinner->message}"); From 07643058f67c0d82d9f66630b1e0dd13e4d63682 Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 23 Sep 2023 19:01:48 +0000 Subject: [PATCH 10/17] Fix code styling --- src/Spinner.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Spinner.php b/src/Spinner.php index 457663ac..33553592 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -59,7 +59,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -114,7 +114,7 @@ protected function renderStreamedOutput(): void $this->eraseDown(); collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); $this->writeDirectly($this->prevFrame); } @@ -202,7 +202,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (!empty($this->pid)) { + if (! empty($this->pid)) { posix_kill($this->pid, SIGHUP); } From 2148b923b13f0595840dcf77321b1fe09361289a Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 21 Oct 2023 14:05:27 -0400 Subject: [PATCH 11/17] consistent output for streaming spinner --- playground/streaming-spinner-process.php | 18 ++++++++++ playground/streaming-spinner.php | 43 ++++++++++++---------- src/Prompt.php | 8 ++--- src/Spinner.php | 46 ++++++++++++++++++------ src/SpinnerMessenger.php | 13 ++++--- src/SpinnerSockets.php | 31 ++++++++++------ src/Themes/Default/SpinnerRenderer.php | 6 +++- 7 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 playground/streaming-spinner-process.php diff --git a/playground/streaming-spinner-process.php b/playground/streaming-spinner-process.php new file mode 100644 index 00000000..cabdda24 --- /dev/null +++ b/playground/streaming-spinner-process.php @@ -0,0 +1,18 @@ +start(); foreach ($process as $type => $data) { - if ($process::ERR === $type) { - $messenger->output($data); - } + $messenger->output($data); } return 'Callback return'; @@ -23,21 +22,27 @@ function (SpinnerMessenger $messenger) { 'Updating Composer...', ); -spin( - function (SpinnerMessenger $messenger) { - foreach (range(1, 50) as $i) { - $messenger->line("✔︎ Step {$i}"); +foreach (range(1, 3) as $i) { + if ($argv[1] ?? false) { + text('Name ' . $i, 'Default ' . $i); + } - usleep(rand(50_000, 250_000)); + spin( + function (SpinnerMessenger $messenger) { + foreach (range(1, 50) as $i) { + $messenger->line("✔︎ Step {$i}"); - if ($i === 20) { - $messenger->message('Almost there...'); - } + usleep(rand(50_000, 250_000)); - if ($i === 35) { - $messenger->message('Still going...'); + if ($i === 20) { + $messenger->message('Almost there...'); + } + + if ($i === 35) { + $messenger->message('Still going...'); + } } - } - }, - 'Taking necessary steps...', -); + }, + 'Taking necessary steps...', + ); +} diff --git a/src/Prompt.php b/src/Prompt.php index 0113f16e..a19b0621 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -77,7 +77,7 @@ public function prompt(): mixed try { static::$interactive ??= stream_isatty(STDIN); - if (! static::$interactive) { + if (!static::$interactive) { return $this->default(); } @@ -275,7 +275,7 @@ private function diffLines(string $a, string $b): array $diff = []; for ($i = 0; $i < max(count($aLines), count($bLines)); $i++) { - if (! isset($aLines[$i]) || ! isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { + if (!isset($aLines[$i]) || !isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { $diff[] = $i; } } @@ -325,13 +325,13 @@ private function validate(mixed $value): void return; } - if (! isset($this->validate)) { + if (!isset($this->validate)) { return; } $error = ($this->validate)($value); - if (! is_string($error) && ! is_null($error)) { + if (!is_string($error) && !is_null($error)) { throw new \RuntimeException('The validator must return a string or null.'); } diff --git a/src/Spinner.php b/src/Spinner.php index 33553592..6bdfa11c 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -37,12 +37,17 @@ class Spinner extends Prompt */ public bool $hasStreamingOutput = false; + /** + * A unique string to indicate that the spinner should stop. + */ + public string $stopIndicator; + /** * Create a new Spinner instance. */ public function __construct(public string $message = '') { - // + $this->stopIndicator = uniqid() . uniqid() . uniqid(); } /** @@ -59,7 +64,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (! function_exists('pcntl_fork')) { + if (!function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -86,9 +91,18 @@ public function spin(Closure $callback): mixed } else { $result = $callback($this->sockets->messenger()); + $this->sockets->messenger()->stop($this->stopIndicator); + // Let the spinner finish its last cycle before exiting usleep($this->interval * 1000); + // Read the last frame actually rendered from the spinner + $realPrevFrame = $this->sockets->readPrevFrame(); + + if ($realPrevFrame) { + $this->prevFrame = $realPrevFrame; + } + $this->resetTerminal($originalAsync); return $result; @@ -107,16 +121,28 @@ protected function renderStreamedOutput(): void { $output = $this->sockets->streamingOutput(); - if ($output !== '') { - $this->hasStreamingOutput = true; + if ($output === '') { + return; + } + + $this->resetCursorPosition(); + $this->eraseDown(); + + if (!$this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { + // This is the first line of streaming output we're about to write + static::writeDirectly(PHP_EOL); + } + + $this->hasStreamingOutput = true; - $this->resetCursorPosition(); - $this->eraseDown(); + collect(explode(PHP_EOL, rtrim($output))) + ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); - collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); + $this->writeDirectly($this->prevFrame); - $this->writeDirectly($this->prevFrame); + if (str_contains($output, $this->stopIndicator)) { + // Send the last frame actually rendered back to the parent process + $this->sockets->sendPrevFrame($this->prevFrame); } } @@ -202,7 +228,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (! empty($this->pid)) { + if (!empty($this->pid)) { posix_kill($this->pid, SIGHUP); } diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index 5066295d..d44dee0f 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -4,22 +4,27 @@ class SpinnerMessenger { - public function __construct(protected Connection $socket, protected Connection $labelSocket) + public function __construct(protected Connection $outputSocket, protected Connection $messageSocket) { } public function output(string $message): void { - $this->socket->write($message); + $this->outputSocket->write($message); } public function line(string $message): void { - $this->output($message.PHP_EOL); + $this->output($message . PHP_EOL); } public function message(string $message): void { - $this->labelSocket->write($message); + $this->messageSocket->write($message); + } + + public function stop(string $stopIndicator) + { + $this->line($stopIndicator); } } diff --git a/src/SpinnerSockets.php b/src/SpinnerSockets.php index 640ef7d1..037b38e2 100644 --- a/src/SpinnerSockets.php +++ b/src/SpinnerSockets.php @@ -42,13 +42,7 @@ public function messenger(): SpinnerMessenger */ public function streamingOutput(): string { - $output = ''; - - foreach ($this->outputToTask->read() as $chunk) { - $output .= $chunk; - } - - return $output; + return $this->getSocketOutput($this->outputToTask); } /** @@ -56,13 +50,28 @@ public function streamingOutput(): string */ public function message(): string { - $message = ''; + return $this->getSocketOutput($this->messageToTask); + } + + public function sendPrevFrame(string $prevFrame) + { + $this->outputToTask->write($prevFrame); + } - foreach ($this->messageToTask->read() as $chunk) { - $message .= $chunk; + public function readPrevFrame(): string + { + return $this->getSocketOutput($this->outputToSpinner); + } + + protected function getSocketOutput($socket) + { + $output = ''; + + foreach ($socket->read() as $chunk) { + $output .= $chunk; } - return $message; + return $output; } /** diff --git a/src/Themes/Default/SpinnerRenderer.php b/src/Themes/Default/SpinnerRenderer.php index 8a5d49d8..0a6ce589 100644 --- a/src/Themes/Default/SpinnerRenderer.php +++ b/src/Themes/Default/SpinnerRenderer.php @@ -35,7 +35,11 @@ public function __invoke(Spinner $spinner): string $spinner->interval = $this->interval; if ($spinner->hasStreamingOutput) { - $this->line($this->dim(str_repeat('─', 63))); + if ($spinner->newLinesWritten() > 1) { + $this->newLine(); + } + + $this->line($this->dim(str_repeat('─', $spinner->terminal()->cols() - 6))); } $frame = $this->frames[$spinner->count % count($this->frames)]; From 53695b15360c80ace50c1035b5d855ec3e6bbbcd Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 21 Oct 2023 18:05:58 +0000 Subject: [PATCH 12/17] Fix code styling --- playground/streaming-spinner-process.php | 2 +- playground/streaming-spinner.php | 6 +++--- src/Prompt.php | 8 ++++---- src/Spinner.php | 10 +++++----- src/SpinnerMessenger.php | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/playground/streaming-spinner-process.php b/playground/streaming-spinner-process.php index cabdda24..3fb69ce5 100644 --- a/playground/streaming-spinner-process.php +++ b/playground/streaming-spinner-process.php @@ -13,6 +13,6 @@ function generateRandomString($length) } foreach (range(0, 50) as $i) { - echo $i . ' ' . generateRandomString(rand(1, 100)) . PHP_EOL; + echo $i.' '.generateRandomString(rand(1, 100)).PHP_EOL; usleep(rand(50_000, 250_000)); } diff --git a/playground/streaming-spinner.php b/playground/streaming-spinner.php index 09dc65eb..22ec174e 100644 --- a/playground/streaming-spinner.php +++ b/playground/streaming-spinner.php @@ -6,11 +6,11 @@ use function Laravel\Prompts\spin; use function Laravel\Prompts\text; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__.'/../vendor/autoload.php'; spin( function (SpinnerMessenger $messenger) { - $process = Process::fromShellCommandline('php ' . __DIR__ . '/streaming-spinner-process.php'); + $process = Process::fromShellCommandline('php '.__DIR__.'/streaming-spinner-process.php'); $process->start(); foreach ($process as $type => $data) { @@ -24,7 +24,7 @@ function (SpinnerMessenger $messenger) { foreach (range(1, 3) as $i) { if ($argv[1] ?? false) { - text('Name ' . $i, 'Default ' . $i); + text('Name '.$i, 'Default '.$i); } spin( diff --git a/src/Prompt.php b/src/Prompt.php index a19b0621..0113f16e 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -77,7 +77,7 @@ public function prompt(): mixed try { static::$interactive ??= stream_isatty(STDIN); - if (!static::$interactive) { + if (! static::$interactive) { return $this->default(); } @@ -275,7 +275,7 @@ private function diffLines(string $a, string $b): array $diff = []; for ($i = 0; $i < max(count($aLines), count($bLines)); $i++) { - if (!isset($aLines[$i]) || !isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { + if (! isset($aLines[$i]) || ! isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { $diff[] = $i; } } @@ -325,13 +325,13 @@ private function validate(mixed $value): void return; } - if (!isset($this->validate)) { + if (! isset($this->validate)) { return; } $error = ($this->validate)($value); - if (!is_string($error) && !is_null($error)) { + if (! is_string($error) && ! is_null($error)) { throw new \RuntimeException('The validator must return a string or null.'); } diff --git a/src/Spinner.php b/src/Spinner.php index 6bdfa11c..b07e2069 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -47,7 +47,7 @@ class Spinner extends Prompt */ public function __construct(public string $message = '') { - $this->stopIndicator = uniqid() . uniqid() . uniqid(); + $this->stopIndicator = uniqid().uniqid().uniqid(); } /** @@ -64,7 +64,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -128,7 +128,7 @@ protected function renderStreamedOutput(): void $this->resetCursorPosition(); $this->eraseDown(); - if (!$this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { + if (! $this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { // This is the first line of streaming output we're about to write static::writeDirectly(PHP_EOL); } @@ -136,7 +136,7 @@ protected function renderStreamedOutput(): void $this->hasStreamingOutput = true; collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); $this->writeDirectly($this->prevFrame); @@ -228,7 +228,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (!empty($this->pid)) { + if (! empty($this->pid)) { posix_kill($this->pid, SIGHUP); } diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index d44dee0f..e74cf6ff 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -15,7 +15,7 @@ public function output(string $message): void public function line(string $message): void { - $this->output($message . PHP_EOL); + $this->output($message.PHP_EOL); } public function message(string $message): void From 8bdc73f6fdc177717b8e2eaf2df0e44da72d5721 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 21 Oct 2023 15:18:33 -0400 Subject: [PATCH 13/17] Update streaming-spinner.php --- playground/streaming-spinner.php | 44 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/playground/streaming-spinner.php b/playground/streaming-spinner.php index 22ec174e..6a902b3c 100644 --- a/playground/streaming-spinner.php +++ b/playground/streaming-spinner.php @@ -6,11 +6,15 @@ use function Laravel\Prompts\spin; use function Laravel\Prompts\text; -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; + +if ($argv[1] ?? false) { + text('Name', 'Default'); +} spin( function (SpinnerMessenger $messenger) { - $process = Process::fromShellCommandline('php '.__DIR__.'/streaming-spinner-process.php'); + $process = Process::fromShellCommandline('php ' . __DIR__ . '/streaming-spinner-process.php'); $process->start(); foreach ($process as $type => $data) { @@ -22,27 +26,25 @@ function (SpinnerMessenger $messenger) { 'Updating Composer...', ); -foreach (range(1, 3) as $i) { - if ($argv[1] ?? false) { - text('Name '.$i, 'Default '.$i); - } +if ($argv[1] ?? false) { + text('Name ' . $i, 'Default ' . $i); +} - spin( - function (SpinnerMessenger $messenger) { - foreach (range(1, 50) as $i) { - $messenger->line("✔︎ Step {$i}"); +spin( + function (SpinnerMessenger $messenger) { + foreach (range(1, 50) as $i) { + $messenger->line("✔︎ Step {$i}"); - usleep(rand(50_000, 250_000)); + usleep(rand(50_000, 250_000)); - if ($i === 20) { - $messenger->message('Almost there...'); - } + if ($i === 20) { + $messenger->message('Almost there...'); + } - if ($i === 35) { - $messenger->message('Still going...'); - } + if ($i === 35) { + $messenger->message('Still going...'); } - }, - 'Taking necessary steps...', - ); -} + } + }, + 'Taking necessary steps...', +); From f5ae4e10dbbd79dbcbd8311efa2a5af5123053af Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 21 Oct 2023 19:19:08 +0000 Subject: [PATCH 14/17] Fix code styling --- playground/streaming-spinner.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/streaming-spinner.php b/playground/streaming-spinner.php index 6a902b3c..2cab36cf 100644 --- a/playground/streaming-spinner.php +++ b/playground/streaming-spinner.php @@ -6,7 +6,7 @@ use function Laravel\Prompts\spin; use function Laravel\Prompts\text; -require __DIR__ . '/../vendor/autoload.php'; +require __DIR__.'/../vendor/autoload.php'; if ($argv[1] ?? false) { text('Name', 'Default'); @@ -14,7 +14,7 @@ spin( function (SpinnerMessenger $messenger) { - $process = Process::fromShellCommandline('php ' . __DIR__ . '/streaming-spinner-process.php'); + $process = Process::fromShellCommandline('php '.__DIR__.'/streaming-spinner-process.php'); $process->start(); foreach ($process as $type => $data) { @@ -27,7 +27,7 @@ function (SpinnerMessenger $messenger) { ); if ($argv[1] ?? false) { - text('Name ' . $i, 'Default ' . $i); + text('Name '.$i, 'Default '.$i); } spin( From e8759e52f2a84fa6d028b2109461580c0175bdaf Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 21 Oct 2023 15:42:30 -0400 Subject: [PATCH 15/17] cleaning up and documenting --- playground/streaming-spinner-process.php | 4 ++-- src/Spinner.php | 22 ++++++++++------------ src/SpinnerMessenger.php | 15 ++++++++++++++- src/SpinnerSockets.php | 13 +++++++++++-- src/Themes/Default/SpinnerRenderer.php | 1 + 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/playground/streaming-spinner-process.php b/playground/streaming-spinner-process.php index 3fb69ce5..5870f316 100644 --- a/playground/streaming-spinner-process.php +++ b/playground/streaming-spinner-process.php @@ -13,6 +13,6 @@ function generateRandomString($length) } foreach (range(0, 50) as $i) { - echo $i.' '.generateRandomString(rand(1, 100)).PHP_EOL; - usleep(rand(50_000, 250_000)); + echo $i . ' ' . generateRandomString(rand(1, 100)) . PHP_EOL; + usleep(rand(10_000, 250_000)); } diff --git a/src/Spinner.php b/src/Spinner.php index b07e2069..11591f70 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -47,7 +47,7 @@ class Spinner extends Prompt */ public function __construct(public string $message = '') { - $this->stopIndicator = uniqid().uniqid().uniqid(); + $this->stopIndicator = uniqid() . uniqid() . uniqid(); } /** @@ -64,7 +64,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (! function_exists('pcntl_fork')) { + if (!function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -91,15 +91,14 @@ public function spin(Closure $callback): mixed } else { $result = $callback($this->sockets->messenger()); + // Tell the child process to stop and send back it's last frame $this->sockets->messenger()->stop($this->stopIndicator); // Let the spinner finish its last cycle before exiting usleep($this->interval * 1000); // Read the last frame actually rendered from the spinner - $realPrevFrame = $this->sockets->readPrevFrame(); - - if ($realPrevFrame) { + if ($realPrevFrame = $this->sockets->prevFrame()) { $this->prevFrame = $realPrevFrame; } @@ -128,15 +127,16 @@ protected function renderStreamedOutput(): void $this->resetCursorPosition(); $this->eraseDown(); - if (! $this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { - // This is the first line of streaming output we're about to write + if (!$this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { + // This is the first line of streaming output we're about to write, if the + // previous frame started with a new line, we need to write a new line. static::writeDirectly(PHP_EOL); } $this->hasStreamingOutput = true; collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); + ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); $this->writeDirectly($this->prevFrame); @@ -151,9 +151,7 @@ protected function renderStreamedOutput(): void */ protected function setNewMessage(): void { - $message = $this->sockets->message(); - - if ($message !== '') { + if (($message = $this->sockets->message()) !== '') { $this->message = $message; } } @@ -228,7 +226,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (! empty($this->pid)) { + if (!empty($this->pid)) { posix_kill($this->pid, SIGHUP); } diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index e74cf6ff..86f8fa59 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -6,23 +6,36 @@ class SpinnerMessenger { public function __construct(protected Connection $outputSocket, protected Connection $messageSocket) { + // } + /** + * Write a message to the output socket. + */ public function output(string $message): void { $this->outputSocket->write($message); } + /** + * Write a message to the output socket with a new line. + */ public function line(string $message): void { - $this->output($message.PHP_EOL); + $this->output($message . PHP_EOL); } + /** + * Write a message to the message socket. + */ public function message(string $message): void { $this->messageSocket->write($message); } + /** + * Write the stop indicator to the output socket. + */ public function stop(string $stopIndicator) { $this->line($stopIndicator); diff --git a/src/SpinnerSockets.php b/src/SpinnerSockets.php index 037b38e2..c950ed87 100644 --- a/src/SpinnerSockets.php +++ b/src/SpinnerSockets.php @@ -53,17 +53,26 @@ public function message(): string return $this->getSocketOutput($this->messageToTask); } + /** + * Send the previous frame back to the parent process. + */ public function sendPrevFrame(string $prevFrame) { $this->outputToTask->write($prevFrame); } - public function readPrevFrame(): string + /** + * Read the previous frame from the spinner. + */ + public function prevFrame(): string { return $this->getSocketOutput($this->outputToSpinner); } - protected function getSocketOutput($socket) + /** + * Read the output from the given socket. + */ + protected function getSocketOutput(Connection $socket) { $output = ''; diff --git a/src/Themes/Default/SpinnerRenderer.php b/src/Themes/Default/SpinnerRenderer.php index 0a6ce589..3496c780 100644 --- a/src/Themes/Default/SpinnerRenderer.php +++ b/src/Themes/Default/SpinnerRenderer.php @@ -36,6 +36,7 @@ public function __invoke(Spinner $spinner): string if ($spinner->hasStreamingOutput) { if ($spinner->newLinesWritten() > 1) { + // Make sure there is always one space above the dividing line. $this->newLine(); } From 2000c2fe7ddedfb3844fbb262991ccfaa77db3f9 Mon Sep 17 00:00:00 2001 From: Joe Tannenbaum Date: Sat, 21 Oct 2023 15:44:11 -0400 Subject: [PATCH 16/17] Update helpers.php --- src/helpers.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index 5ffb94ae..93557e09 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Support\Collection; +use Laravel\Prompts\SpinnerMessenger; /** * Prompt the user for text input. @@ -89,7 +90,7 @@ function multisearch(string $label, Closure $options, string $placeholder = '', * * @template TReturn of mixed * - * @param \Closure(): TReturn $callback + * @param \Closure(SpinnerMessenger): TReturn $callback * @return TReturn */ function spin(Closure $callback, string $message = ''): mixed From f6587a5fd194a769c9f7ff74cae4f0a811fd8c76 Mon Sep 17 00:00:00 2001 From: joetannenbaum Date: Sat, 21 Oct 2023 19:58:04 +0000 Subject: [PATCH 17/17] Fix code styling --- playground/streaming-spinner-process.php | 2 +- src/Spinner.php | 10 +++++----- src/SpinnerMessenger.php | 2 +- src/helpers.php | 1 - 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/playground/streaming-spinner-process.php b/playground/streaming-spinner-process.php index 5870f316..2e711ec2 100644 --- a/playground/streaming-spinner-process.php +++ b/playground/streaming-spinner-process.php @@ -13,6 +13,6 @@ function generateRandomString($length) } foreach (range(0, 50) as $i) { - echo $i . ' ' . generateRandomString(rand(1, 100)) . PHP_EOL; + echo $i.' '.generateRandomString(rand(1, 100)).PHP_EOL; usleep(rand(10_000, 250_000)); } diff --git a/src/Spinner.php b/src/Spinner.php index 11591f70..bfbab1c3 100644 --- a/src/Spinner.php +++ b/src/Spinner.php @@ -47,7 +47,7 @@ class Spinner extends Prompt */ public function __construct(public string $message = '') { - $this->stopIndicator = uniqid() . uniqid() . uniqid(); + $this->stopIndicator = uniqid().uniqid().uniqid(); } /** @@ -64,7 +64,7 @@ public function spin(Closure $callback): mixed $this->sockets = SpinnerSockets::create(); - if (!function_exists('pcntl_fork')) { + if (! function_exists('pcntl_fork')) { return $this->renderStatically($callback); } @@ -127,7 +127,7 @@ protected function renderStreamedOutput(): void $this->resetCursorPosition(); $this->eraseDown(); - if (!$this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { + if (! $this->hasStreamingOutput && str_starts_with($this->prevFrame, PHP_EOL)) { // This is the first line of streaming output we're about to write, if the // previous frame started with a new line, we need to write a new line. static::writeDirectly(PHP_EOL); @@ -136,7 +136,7 @@ protected function renderStreamedOutput(): void $this->hasStreamingOutput = true; collect(explode(PHP_EOL, rtrim($output))) - ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' ' . $line . PHP_EOL)); + ->each(fn ($line) => $line === $this->stopIndicator ? null : static::writeDirectlyWithFormatting(' '.$line.PHP_EOL)); $this->writeDirectly($this->prevFrame); @@ -226,7 +226,7 @@ protected function eraseRenderedLines(): void */ public function __destruct() { - if (!empty($this->pid)) { + if (! empty($this->pid)) { posix_kill($this->pid, SIGHUP); } diff --git a/src/SpinnerMessenger.php b/src/SpinnerMessenger.php index 86f8fa59..b8f0b3fa 100644 --- a/src/SpinnerMessenger.php +++ b/src/SpinnerMessenger.php @@ -22,7 +22,7 @@ public function output(string $message): void */ public function line(string $message): void { - $this->output($message . PHP_EOL); + $this->output($message.PHP_EOL); } /** diff --git a/src/helpers.php b/src/helpers.php index 93557e09..5e9c186f 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,7 +4,6 @@ use Closure; use Illuminate\Support\Collection; -use Laravel\Prompts\SpinnerMessenger; /** * Prompt the user for text input.