diff --git a/playground/watch.php b/playground/watch.php new file mode 100644 index 00000000..d7cd6fda --- /dev/null +++ b/playground/watch.php @@ -0,0 +1,28 @@ +format(DateTime::RFC850)]; + + table( + [ + 'Iteration', + 'DateTime' + ], + $items + ); + }, + 1, +); diff --git a/src/Concerns/Themes.php b/src/Concerns/Themes.php index f2fe3b57..a544e972 100644 --- a/src/Concerns/Themes.php +++ b/src/Concerns/Themes.php @@ -29,6 +29,8 @@ use Laravel\Prompts\Themes\Default\SuggestPromptRenderer; use Laravel\Prompts\Themes\Default\TableRenderer; use Laravel\Prompts\Themes\Default\TextPromptRenderer; +use Laravel\Prompts\Themes\Default\BufferedOutputRenderer; +use Laravel\Prompts\Watch; trait Themes { @@ -57,6 +59,7 @@ trait Themes Note::class => NoteRenderer::class, Table::class => TableRenderer::class, Progress::class => ProgressRenderer::class, + Watch::class => BufferedOutputRenderer::class, ], ]; diff --git a/src/Contracts/Buffered.php b/src/Contracts/Buffered.php new file mode 100644 index 00000000..a495e30c --- /dev/null +++ b/src/Contracts/Buffered.php @@ -0,0 +1,8 @@ +buffer(); + + $watch::setOutput(static::$previousOutput ?? new ConsoleOutput()); + + return $bufferedOutput->fetch(); + } +} diff --git a/src/Watch.php b/src/Watch.php new file mode 100644 index 00000000..68a4d3ca --- /dev/null +++ b/src/Watch.php @@ -0,0 +1,148 @@ +watch = $watch(...); + $this->interval = $interval ?? 2; + + if ($this->interval < 0) { + throw new ValueError('watch interval must be greater than or equal to 0'); + } + } + + /** + * displays the watched output and updates after the specified interval. + */ + public function display(): void + { + $faked = static::output() instanceof BufferedConsoleOutput; + + while (!$faked || $this->fakedTimes < static::$fakeTimes) { + BufferedOutputRenderer::setPreviousOutput( + static::output() + ); + + $this->render(); + + if ($faked) { + $this->fakedTimes++; + + if ($this->fakedTimes >= static::$fakeTimes) { + static::$fakeSleep = true; + break; + } + + if (static::$fakeSleep) { + static::$sleptSeconds += $this->interval; + continue; + } + } + + sleep($this->interval); + } + } + + /** + * Buffers the output from the callable and flushes the output to Prompts + * in order to utilize Prompts neat way of updating lines + */ + public function buffer(): void + { + ($this->watch)(); + } + + /** + * overrides prompt so it will have the same behavior. + */ + public function prompt(): bool + { + $this->display(); + + return true; + } + + /** + * Get the value of the prompt. + */ + public function value(): bool + { + return true; + } + + /** + * Tell Prompt how many iterations to fake + */ + public static function fakeTimes(int $times): void + { + if (!static::output() instanceof BufferedConsoleOutput) { + throw new RuntimeException('Prompt must be faked before faking iterations.'); + } + + static::$fakeTimes = $times; + } + + /** + * Asserts the amount of seconds slept during intervals in total. + */ + public static function assertSecondsSleptBetweenIntervals(int $seconds): void + { + if (!static::output() instanceof BufferedConsoleOutput) { + throw new RuntimeException('Prompt must be faked before asserting.'); + } + + Assert::assertEquals($seconds, static::$sleptSeconds); + } + + /** + * By default, when Prompt is faked, the intervals are faked.. + * This tells Prompt to actually sleep between updates. + */ + public static function shouldNotFakeIntervalSleep(): void + { + if (!static::output() instanceof BufferedConsoleOutput) { + throw new RuntimeException('Not faking sleep makes no sense when not faking Prompt.'); + } + + static::$fakeSleep = false; + } +} diff --git a/src/helpers.php b/src/helpers.php index 8c05b784..a9dd28e6 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -192,3 +192,13 @@ function progress(string $label, iterable|int $steps, ?Closure $callback = null, return $progress; } + +/** + * Continuously updates output on each interval. + * + * @param callable(): void $watch + */ +function watch(callable $watch, ?int $interval = 2): void +{ + (new Watch($watch, $interval))->display(); +} diff --git a/tests/Feature/WatchTest.php b/tests/Feature/WatchTest.php new file mode 100644 index 00000000..81690646 --- /dev/null +++ b/tests/Feature/WatchTest.php @@ -0,0 +1,144 @@ + Watch::output()) + ->bindTo(null, Watch::class)()->fetch(); + }); + + Prompt::assertOutputDoesntContain('This should not render'); +}); + +it('it should fake sleep when faking', function ( + int $expected, + int $iteration, + int $interval = null +) { + Prompt::fake(); + + Watch::fakeTimes($iteration); + + watch(function () { + }, $interval); + + Watch::assertSecondsSleptBetweenIntervals($expected); +})->with( + [ + ['expected' => 2, 'iteration' => 2, 'interval' => 2], + ['expected' => 3, 'iteration' => 2, 'interval' => 3], + ['expected' => 6, 'iteration' => 3, 'interval' => 3], + ['expected' => 4, 'iteration' => 3, 'interval' => null], + ] +); + +it('should throw exception with a negative interval ', function () { + Prompt::fake(); + + watch(fn() => null, -1); + +})->throws(ValueError::class); + +it('should sleep 2 seconds by default', function () { + Prompt::fake(); + + Watch::fakeTimes(2); + + watch(function () { + }); + + Watch::assertSecondsSleptBetweenIntervals(2); +}); + +it('should actually sleep at intervals', function () { + + Prompt::fake(); + + Watch::fakeTimes(2); + + Watch::shouldNotFakeIntervalSleep(); + + $start = time(); + + watch(function () { + note('This should render'); + }, 1); + + $end = time(); + + expect($end - $start)->toBe(1); + + Prompt::assertOutputContains('This should render'); +}); + +it('should throw exception when invoking fakeTimes when not faked', function () { + (function () { + Prompt::$output = new ConsoleOutput(); + })->bindTo(null, Prompt::class)(); + Watch::fakeTimes(2); +})->throws(RuntimeException::class); + +it('should throw exception when invoking assertSlept when not faked', function () { + + (function () { + Prompt::$output = new ConsoleOutput(); + })->bindTo(null, Prompt::class)(); + + Watch::assertSecondsSleptBetweenIntervals(2); +})->throws(RuntimeException::class);