Skip to content

Commit

Permalink
[ Task ] Remove Patched Prompt (#8)
Browse files Browse the repository at this point in the history
* move laravel/prompts to require-dev, suggest install, bump version

* require illuminate/collections

* require react/event-loop

* sort composer packages

* Add Override attirbutes to AsyncConsoleOutput

* remove PatchedPrompt, extend and Override Prompt class

* track trailing newlines in AsyncConsoleOutput

* Fix code styling

* drop php 8.1 support

* add warning to docs

---------

Co-authored-by: ProjektGopher <[email protected]>
  • Loading branch information
ProjektGopher and ProjektGopher authored Oct 2, 2024
1 parent 7c1c37f commit 01cb1d0
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 531 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.1, 8.2, 8.3]
php: [8.2, 8.3]

name: PHP ${{ matrix.php }}

Expand Down
7 changes: 4 additions & 3 deletions Docs/async-prompts.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Asynchronous Prompts

In a standard Laravel Prompt, the looping mechanism blocks the main thread while waiting for keypresses. This limits our ability to trigger renders using an `event` based approach.
> [!WARNING]
> Using this `AsyncPrompt` class will **modify** the `blocking` mode on `STDIN`, which can have _unexpected_ consequences when switching back to a standard `Prompt`.
In `ArtisanBuild/CommunityPrompts/PatchedPrompt` we've extracted these looping mechanisms. This class will not be needed if [this compatibility PR](https://github.com/laravel/prompts/pull/154) can be re-opened and merged.
In a standard Laravel Prompt, the looping mechanism blocks the main thread while waiting for keypresses. This limits our ability to trigger renders using an `event` based approach.

By extracting these looping mechanisms we're able to overwrite the implementations of these loops in `ArtisanBuild/CommunityPrompts/AsyncPrompt` using a [ReactPHP](https://reactphp.org/) event loop. This unlocks the ability for us to read the terminal, write to the terminal, dispatch http requests, etc, in a non-blocking way. By calling the `render()` method inside `callbacks` we can now do things like debounce http requests to search endpoints, output streamed http responses to the terminal one chunk at a time without blocking the user from entering new text, or even listening for real-time push notifications from a websocket.
By **overwriting** these looping mechanisms in `ArtisanBuild/CommunityPrompts/AsyncPrompt` using a [ReactPHP](https://reactphp.org/) event loop. This unlocks the ability for us to read the terminal, write to the terminal, dispatch http requests, etc, in a non-blocking way. By calling the `render()` method inside `callbacks` we can now do things like debounce http requests to search endpoints, output streamed http responses to the terminal one chunk at a time without blocking the user from entering new text, or even listening for real-time push notifications from a websocket.
11 changes: 8 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,20 @@
}
},
"require": {
"laravel/prompts": "^0.1.24",
"illuminate/collections": "^11.26",
"react/event-loop": "^1.5",
"react/stream": "^1.4"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"pestphp/pest": "^2.3",
"laravel/prompts": "^0.3.0",
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-mockery": "^1.1"
},
"suggest": {
"laravel/prompts": "Required for the abstract Prompt class."
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
Expand Down
41 changes: 30 additions & 11 deletions src/AsyncPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
namespace ArtisanBuild\CommunityPrompts;

use ArtisanBuild\CommunityPrompts\Output\AsyncConsoleOutput;
use Closure;
use Laravel\Prompts\Output\BufferedConsoleOutput;
use Laravel\Prompts\Prompt;
use Laravel\Prompts\Support\Result;
use Override;
use React\EventLoop\Loop;
use React\Stream\ReadableResourceStream;
use Symfony\Component\Console\Output\OutputInterface;

abstract class AsyncPrompt extends PatchedPrompt
abstract class AsyncPrompt extends Prompt
{
/**
* The output instance.
Expand All @@ -20,45 +23,61 @@ abstract class AsyncPrompt extends PatchedPrompt

protected static ReadableResourceStream $stdin;

/**
* Implementation for the looping mechanism for faking keypresses
*
* @param array<int, string> $keys
*/
public static function fakeKeyPresses(array $keys, Closure $closure): void
#[Override]
public static function fakeKeyPresses(array $keys, callable $closure): void
{
static::$stdin ??= new ReadableResourceStream(STDIN);
foreach ($keys as $key) {
Loop::get()->futureTick(function () use ($key) {
static::$stdin->emit('data', [$key]);
});
}

self::setOutput(new BufferedConsoleOutput);
}

#[Override]
public function runLoop(callable $callable): mixed
{
/**
* @var Result|null $result
*/
$result = null;

static::$stdin ??= new ReadableResourceStream(STDIN);
static::$stdin->on('data', function (string $key) use ($callable, &$result) {
$result = $callable($key);

if (! $this->is_nothing($result)) {
if ($result instanceof Result) {
Loop::stop();
}
});

Loop::run();
static::$stdin->removeAllListeners();

return $result;
if ($result === null) {
throw new \RuntimeException('Prompt did not return a result.');
}

return $result->value;
}

/**
* Set the output instance.
*/
#[Override]
public static function setOutput(OutputInterface $output): void
{
self::$output = $output;
}

/**
* Get the current output instance.
*/
#[Override]
protected static function output(): OutputInterface
{
return static::$output ??= new AsyncConsoleOutput;
return self::$output ??= new AsyncConsoleOutput;
}
}
15 changes: 13 additions & 2 deletions src/Output/AsyncConsoleOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ArtisanBuild\CommunityPrompts\Output;

use Laravel\Prompts\Output\ConsoleOutput;
use Override;
use React\Stream\WritableResourceStream;

class AsyncConsoleOutput extends ConsoleOutput
Expand All @@ -12,19 +13,29 @@ class AsyncConsoleOutput extends ConsoleOutput
/**
* Write to the output buffer.
*/
#[Override]
protected function doWrite(string $message, bool $newline): void
{
if ($newline) {
$message .= PHP_EOL;
}

$this->stdout ??= new WritableResourceStream(STDOUT);
$this->stdout->write($message);

if ($newline) {
$this->stdout->write(PHP_EOL);
$trailingNewLines = strlen($message) - strlen(rtrim($message, PHP_EOL));

if (trim($message) === '') {
$this->newLinesWritten += $trailingNewLines;
} else {
$this->newLinesWritten = $trailingNewLines;
}
}

/**
* Write output directly, bypassing newline capture.
*/
#[Override]
public function writeDirectly(string $message): void
{
$this->doWrite($message, false);
Expand Down
Loading

0 comments on commit 01cb1d0

Please sign in to comment.