Skip to content

Commit

Permalink
Extract Looping Mechanisms (#162)
Browse files Browse the repository at this point in the history
* extract looping mechanism in prompt

* extract keypress looping mechanism in FakesInputOutput

* add docblock with type

* swap out `Nothing` class for proper `Result` sentinel class

* formatting

* swap out Closure for callable, add types

* add documentation

* add comment

* move readonly keyword to property for php8.1 support
  • Loading branch information
ProjektGopher authored Sep 11, 2024
1 parent fa9674f commit fdc964d
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 6 deletions.
21 changes: 19 additions & 2 deletions src/Concerns/FakesInputOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,32 @@ public static function fake(array $keys = []): void
$mock->shouldReceive('lines')->byDefault()->andReturn(24);
$mock->shouldReceive('initDimensions')->byDefault();

foreach ($keys as $key) {
static::fakeKeyPresses($keys, function (string $key) use ($mock): void {
$mock->shouldReceive('read')->once()->andReturn($key);
}
});

static::$terminal = $mock;

self::setOutput(new BufferedConsoleOutput);
}

/**
* Implementation of the looping mechanism for simulating key presses.
*
* By ignoring the `$callable` parameter which contains the default logic
* for simulating fake key presses, we can use a custom implementation
* to emit key presses instead, allowing us to use different inputs.
*
* @param array<string> $keys
* @param callable(string $key): void $callable
*/
public static function fakeKeyPresses(array $keys, callable $callable): void
{
foreach ($keys as $key) {
$callable($key);
}
}

/**
* Assert that the output contains the given string.
*/
Expand Down
30 changes: 26 additions & 4 deletions src/Prompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Closure;
use Laravel\Prompts\Exceptions\FormRevertedException;
use Laravel\Prompts\Output\ConsoleOutput;
use Laravel\Prompts\Support\Result;
use RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
Expand Down Expand Up @@ -127,15 +128,15 @@ public function prompt(): mixed
$this->hideCursor();
$this->render();

while (($key = static::terminal()->read()) !== null) {
$result = $this->runLoop(function (string $key): ?Result {
$continue = $this->handleKeyPress($key);

$this->render();

if ($continue === false || $key === Key::CTRL_C) {
if ($key === Key::CTRL_C) {
if (isset(static::$cancelUsing)) {
return (static::$cancelUsing)();
return Result::from((static::$cancelUsing)());
} else {
static::terminal()->exit();
}
Expand All @@ -145,14 +146,35 @@ public function prompt(): mixed
throw new FormRevertedException;
}

return $this->transformedValue();
return Result::from($this->transformedValue());
}
}

// Continue looping.
return null;
});

return $result;
} finally {
$this->clearListeners();
}
}

/**
* Implementation of the prompt looping mechanism.
*
* @param callable(string $key): ?Result $callable
*/
public function runLoop(callable $callable): mixed
{
while(($key = static::terminal()->read()) !== null) {
$result = $callable($key);

if ($result instanceof Result) {
return $result->value;
}
}
}

/**
* Register a callback to be invoked when a user cancels a prompt.
*/
Expand Down
22 changes: 22 additions & 0 deletions src/Support/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Laravel\Prompts\Support;

/**
* Result.
*
* This is a 'sentinel' value. It wraps a return value, which can
* allow us to differentiate between a `null` return value and
* a `null` return value that's intended to continue a loop.
*/
final class Result
{
public function __construct(
public readonly mixed $value,
) {}

public static function from(mixed $value): self
{
return new self($value);
}
}

0 comments on commit fdc964d

Please sign in to comment.