Skip to content

Commit

Permalink
Merge branch 'main' into playground/streaming-spinner
Browse files Browse the repository at this point in the history
  • Loading branch information
joetannenbaum committed Sep 23, 2023
2 parents d76d9e0 + ab5afba commit c82a4af
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 63 deletions.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"mockery/mockery": "^1.5",
"phpstan/phpstan-mockery": "^1.1"
},
"conflict": {
"illuminate/console": ">=10.17.0 <10.25.0",
"laravel/framework": ">=10.17.0 <10.25.0"
},
"suggest": {
"ext-pcntl": "Required for the spinner to be animated."
},
Expand All @@ -37,7 +41,7 @@
},
"extra": {
"branch-alias": {
"dev-main": "0.2.x-dev"
"dev-main": "0.1.x-dev"
}
}
}
8 changes: 8 additions & 0 deletions src/Concerns/Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ public function emit(string $event, mixed ...$data): void
$listener(...$data);
}
}

/**
* Clean the event listeners.
*/
public function clearListeners(): void
{
$this->listeners = [];
}
}
72 changes: 39 additions & 33 deletions src/Prompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,52 +74,48 @@ abstract public function value(): mixed;
*/
public function prompt(): mixed
{
static::$interactive ??= stream_isatty(STDIN);

if (! static::$interactive) {
return $this->default();
}
try {
static::$interactive ??= stream_isatty(STDIN);

$this->capturePreviousNewLines();
if (! static::$interactive) {
return $this->default();
}

if (static::shouldFallback()) {
return $this->fallback();
}
$this->capturePreviousNewLines();

$this->checkEnvironment();
if (static::shouldFallback()) {
return $this->fallback();
}

try {
static::terminal()->setTty('-icanon -isig -echo');
} catch (Throwable $e) {
static::output()->writeln("<comment>{$e->getMessage()}</comment>");
static::fallbackWhen(true);
$this->checkEnvironment();

return $this->fallback();
}
try {
static::terminal()->setTty('-icanon -isig -echo');
} catch (Throwable $e) {
static::output()->writeln("<comment>{$e->getMessage()}</comment>");
static::fallbackWhen(true);

register_shutdown_function(function () {
$this->restoreCursor();
static::terminal()->restoreTty();
});
return $this->fallback();
}

$this->hideCursor();
$this->render();
$this->hideCursor();
$this->render();

while (($key = static::terminal()->read()) !== null) {
$continue = $this->handleKeyPress($key);
while (($key = static::terminal()->read()) !== null) {
$continue = $this->handleKeyPress($key);

$this->render();
$this->render();

if ($continue === false || $key === Key::CTRL_C) {
$this->restoreCursor();
static::terminal()->restoreTty();
if ($continue === false || $key === Key::CTRL_C) {
if ($key === Key::CTRL_C) {
static::terminal()->exit();
}

if ($key === Key::CTRL_C) {
static::terminal()->exit();
return $this->value();
}

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

Expand Down Expand Up @@ -354,4 +350,14 @@ private function checkEnvironment(): void
throw new RuntimeException('Prompts is not currently supported on Windows. Please use WSL or configure a fallback.');
}
}

/**
* Restore the cursor and terminal state.
*/
public function __destruct()
{
$this->restoreCursor();

static::terminal()->restoreTty();
}
}
13 changes: 7 additions & 6 deletions src/SearchPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Prompts;

use Closure;
use InvalidArgumentException;

class SearchPrompt extends Prompt
{
Expand All @@ -20,11 +21,6 @@ class SearchPrompt extends Prompt
*/
public int $firstVisible = 0;

/**
* Whether user input is required.
*/
public bool|string $required = true;

/**
* The cached matches.
*
Expand All @@ -43,8 +39,13 @@ public function __construct(
public string $placeholder = '',
public int $scroll = 5,
public ?Closure $validate = null,
public string $hint = ''
public string $hint = '',
public bool|string $required = true,
) {
if ($this->required === false) {
throw new InvalidArgumentException('Argument [required] must be true or a string.');
}

$this->trackTypedValue(submit: false);

$this->reduceScrollingToFitTerminal();
Expand Down
13 changes: 7 additions & 6 deletions src/SelectPrompt.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Illuminate\Support\Collection;
use InvalidArgumentException;

class SelectPrompt extends Prompt
{
Expand All @@ -26,11 +27,6 @@ class SelectPrompt extends Prompt
*/
public array $options;

/**
* Whether user input is required.
*/
public bool|string $required = true;

/**
* Create a new SelectPrompt instance.
*
Expand All @@ -42,8 +38,13 @@ public function __construct(
public int|string|null $default = null,
public int $scroll = 5,
public ?Closure $validate = null,
public string $hint = ''
public string $hint = '',
public bool|string $required = true,
) {
if ($this->required === false) {
throw new InvalidArgumentException('Argument [required] must be true or a string.');
}

$this->options = $options instanceof Collection ? $options->all() : $options;

$this->reduceScrollingToFitTerminal();
Expand Down
33 changes: 21 additions & 12 deletions src/Spinner.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class Spinner extends Prompt
*/
protected SpinnerSockets $sockets;

/**
* The process ID after forking.
*/
protected int $pid;

/**
* Create a new Spinner instance.
*/
Expand All @@ -57,9 +62,7 @@ public function spin(Closure $callback): mixed

$this->sockets = SpinnerSockets::create();

register_shutdown_function(fn () => $this->restoreCursor());

if (! function_exists('pcntl_fork')) {
if (!function_exists('pcntl_fork')) {
return $this->renderStatically($callback);
}

Expand All @@ -71,9 +74,9 @@ public function spin(Closure $callback): mixed
$this->hideCursor();
$this->render();

$pid = pcntl_fork();
$this->pid = pcntl_fork();

if ($pid === 0) {
if ($this->pid === 0) {
while (true) { // @phpstan-ignore-line
$this->setNewMessage();
$this->renderStreamedOutput();
Expand All @@ -84,12 +87,8 @@ public function spin(Closure $callback): mixed
usleep($this->interval * 1000);
}
} else {
register_shutdown_function(fn () => posix_kill($pid, SIGHUP));

$result = $callback($this->sockets->messenger());

posix_kill($pid, SIGHUP);

$this->resetTerminal($originalAsync);

return $result;
Expand Down Expand Up @@ -117,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);
Expand Down Expand Up @@ -147,7 +146,6 @@ protected function resetTerminal(bool $originalAsync): void
$this->sockets->close();

$this->eraseRenderedLines();
$this->showCursor();
}

/**
Expand All @@ -169,7 +167,6 @@ protected function renderStatically(Closure $callback): mixed
$result = $callback();
} finally {
$this->eraseRenderedLines();
$this->showCursor();
}

return $result;
Expand Down Expand Up @@ -202,4 +199,16 @@ protected function eraseRenderedLines(): void
$this->moveCursor(-999, -count($lines) + 1);
$this->eraseDown();
}

/**
* Clean up after the spinner.
*/
public function __destruct()
{
if (!empty($this->pid)) {
posix_kill($this->pid, SIGHUP);
}

parent::__destruct();
}
}
2 changes: 1 addition & 1 deletion src/Terminal.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function setTty(string $mode): void
*/
public function restoreTty(): void
{
if ($this->initialTtyMode) {
if (isset($this->initialTtyMode)) {
$this->exec("stty {$this->initialTtyMode}");

$this->initialTtyMode = null;
Expand Down
10 changes: 6 additions & 4 deletions src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ function password(string $label, string $placeholder = '', bool|string $required
* Prompt the user to select an option.
*
* @param array<int|string, string>|Collection<int|string, string> $options
* @param true|string $required
*/
function select(string $label, array|Collection $options, int|string $default = null, int $scroll = 5, Closure $validate = null, string $hint = ''): int|string
function select(string $label, array|Collection $options, int|string $default = null, int $scroll = 5, Closure $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SelectPrompt($label, $options, $default, $scroll, $validate, $hint))->prompt();
return (new SelectPrompt($label, $options, $default, $scroll, $validate, $hint, $required))->prompt();
}

/**
Expand Down Expand Up @@ -65,10 +66,11 @@ function suggest(string $label, array|Collection|Closure $options, string $place
* Allow the user to search for an option.
*
* @param Closure(string): array<int|string, string> $options
* @param true|string $required
*/
function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, Closure $validate = null, string $hint = ''): int|string
function search(string $label, Closure $options, string $placeholder = '', int $scroll = 5, Closure $validate = null, string $hint = '', bool|string $required = true): int|string
{
return (new SearchPrompt($label, $options, $placeholder, $scroll, $validate, $hint))->prompt();
return (new SearchPrompt($label, $options, $placeholder, $scroll, $validate, $hint, $required))->prompt();
}

/**
Expand Down
6 changes: 6 additions & 0 deletions tests/Feature/SearchPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@

search('What is your favorite color?', fn () => []);
})->throws(NonInteractiveValidationException::class, 'Required.');

it('allows the required validation message to be customised when non-interactive', function () {
Prompt::interactive(false);

search('What is your favorite color?', fn () => [], required: 'The color is required.');
})->throws(NonInteractiveValidationException::class, 'The color is required.');
14 changes: 14 additions & 0 deletions tests/Feature/SelectPromptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,17 @@
validate: fn ($value) => $value === 'None' ? 'Required.' : null,
);
})->throws(NonInteractiveValidationException::class, 'Required.');

it('Allows the required validation message to be customised when non-interactive', function () {
Prompt::interactive(false);

select(
label: 'What is your favorite color?',
options: [
'Red',
'Green',
'Blue',
],
required: 'The color is required.',
);
})->throws(NonInteractiveValidationException::class, 'The color is required.');

0 comments on commit c82a4af

Please sign in to comment.