diff --git a/src/Concerns/Cursor.php b/src/Concerns/Cursor.php index 1a2f1e17..a4d8d1e2 100644 --- a/src/Concerns/Cursor.php +++ b/src/Concerns/Cursor.php @@ -60,4 +60,20 @@ public function moveCursor(int $x, int $y = 0): void static::writeDirectly($sequence); } + + /** + * Move the cursor to the given column. + */ + public function moveCursorToColumn(int $column): void + { + static::writeDirectly("\e[{$column}G"); + } + + /** + * Move the cursor up by the given number of lines. + */ + public function moveCursorUp(int $lines): void + { + static::writeDirectly("\e[{$lines}A"); + } } diff --git a/src/Concerns/FakesInputOutput.php b/src/Concerns/FakesInputOutput.php index 2df265f1..e136db1d 100644 --- a/src/Concerns/FakesInputOutput.php +++ b/src/Concerns/FakesInputOutput.php @@ -27,6 +27,7 @@ public static function fake(array $keys = []): void $mock->shouldReceive('restoreTty')->byDefault(); $mock->shouldReceive('cols')->byDefault()->andReturn(80); $mock->shouldReceive('lines')->byDefault()->andReturn(24); + $mock->shouldReceive('initDimensions')->byDefault(); foreach ($keys as $key) { $mock->shouldReceive('read')->once()->andReturn($key); diff --git a/src/Concerns/Scrolling.php b/src/Concerns/Scrolling.php index f62be7c4..181a825e 100644 --- a/src/Concerns/Scrolling.php +++ b/src/Concerns/Scrolling.php @@ -38,7 +38,7 @@ protected function reduceScrollingToFitTerminal(): void { $reservedLines = ($renderer = $this->getRenderer()) instanceof ScrollingRenderer ? $renderer->reservedLines() : 0; - $this->scroll = min($this->scroll, $this->terminal()->lines() - $reservedLines); + $this->scroll = max(1, min($this->scroll, $this->terminal()->lines() - $reservedLines)); } /** diff --git a/src/Prompt.php b/src/Prompt.php index dfa793c1..099c3bb5 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -208,6 +208,8 @@ public static function validateUsing(Closure $callback): void */ protected function render(): void { + $this->terminal()->initDimensions(); + $frame = $this->renderTheme(); if ($frame === $this->prevFrame) { @@ -223,35 +225,14 @@ protected function render(): void return; } - $this->resetCursorPosition(); - - // Ensure that the full frame is buffered so subsequent output can see how many trailing newlines were written. - if ($this->state === 'submit') { - $this->eraseDown(); - static::output()->write($frame); + $terminalHeight = $this->terminal()->lines(); + $previousFrameHeight = count(explode(PHP_EOL, $this->prevFrame)); + $renderableLines = array_slice(explode(PHP_EOL, $frame), abs(min(0, $terminalHeight - $previousFrameHeight))); - $this->prevFrame = ''; - - return; - } - - $diff = $this->diffLines($this->prevFrame, $frame); - - if (count($diff) === 1) { // Update the single line that changed. - $diffLine = $diff[0]; - $this->moveCursor(0, $diffLine); - $this->eraseLines(1); - $lines = explode(PHP_EOL, $frame); - static::output()->write($lines[$diffLine]); - $this->moveCursor(0, count($lines) - $diffLine - 1); - } elseif (count($diff) > 1) { // Re-render everything past the first change - $diffLine = $diff[0]; - $this->moveCursor(0, $diffLine); - $this->eraseDown(); - $lines = explode(PHP_EOL, $frame); - $newLines = array_slice($lines, $diffLine); - static::output()->write(implode(PHP_EOL, $newLines)); - } + $this->moveCursorToColumn(1); + $this->moveCursorUp(min($terminalHeight, $previousFrameHeight) - 1); + $this->eraseDown(); + $this->output()->write(implode(PHP_EOL, $renderableLines)); $this->prevFrame = $frame; } @@ -268,40 +249,6 @@ protected function submit(): void } } - /** - * Reset the cursor position to the beginning of the previous frame. - */ - private function resetCursorPosition(): void - { - $lines = count(explode(PHP_EOL, $this->prevFrame)) - 1; - - $this->moveCursor(-999, $lines * -1); - } - - /** - * Get the difference between two strings. - * - * @return array - */ - private function diffLines(string $a, string $b): array - { - if ($a === $b) { - return []; - } - - $aLines = explode(PHP_EOL, $a); - $bLines = explode(PHP_EOL, $b); - $diff = []; - - for ($i = 0; $i < max(count($aLines), count($bLines)); $i++) { - if (! isset($aLines[$i]) || ! isset($bLines[$i]) || $aLines[$i] !== $bLines[$i]) { - $diff[] = $i; - } - } - - return $diff; - } - /** * Handle a key press and determine whether to continue. */ diff --git a/src/Terminal.php b/src/Terminal.php index 5cda9bb3..b91fea89 100644 --- a/src/Terminal.php +++ b/src/Terminal.php @@ -2,6 +2,7 @@ namespace Laravel\Prompts; +use ReflectionClass; use RuntimeException; use Symfony\Component\Console\Terminal as SymfonyTerminal; @@ -13,14 +14,17 @@ class Terminal protected ?string $initialTtyMode; /** - * The number of columns in the terminal. + * The Symfony Terminal instance. */ - protected int $cols; + protected SymfonyTerminal $terminal; /** - * The number of lines in the terminal. + * Create a new Terminal instance. */ - protected int $lines; + public function __construct() + { + $this->terminal = new SymfonyTerminal(); + } /** * Read a line from the terminal. @@ -59,7 +63,7 @@ public function restoreTty(): void */ public function cols(): int { - return $this->cols ??= (new SymfonyTerminal())->getWidth(); + return $this->terminal->getWidth(); } /** @@ -67,7 +71,17 @@ public function cols(): int */ public function lines(): int { - return $this->lines ??= (new SymfonyTerminal())->getHeight(); + return $this->terminal->getHeight(); + } + + /** + * (Re)initialize the terminal dimensions. + */ + public function initDimensions(): void + { + (new ReflectionClass($this->terminal)) + ->getMethod('initDimensions') + ->invoke($this->terminal); } /** diff --git a/src/Themes/Default/Renderer.php b/src/Themes/Default/Renderer.php index 61f40afa..9356003c 100644 --- a/src/Themes/Default/Renderer.php +++ b/src/Themes/Default/Renderer.php @@ -5,7 +5,6 @@ use Laravel\Prompts\Concerns\Colors; use Laravel\Prompts\Concerns\Truncation; use Laravel\Prompts\Prompt; -use RuntimeException; abstract class Renderer { @@ -22,7 +21,7 @@ abstract class Renderer */ public function __construct(protected Prompt $prompt) { - $this->checkTerminalSize($prompt); + // } /** @@ -100,19 +99,4 @@ public function __toString() .$this->output .(in_array($this->prompt->state, ['submit', 'cancel']) ? PHP_EOL : ''); } - - /** - * Check that the terminal is large enough to render the prompt. - */ - private function checkTerminalSize(Prompt $prompt): void - { - $required = 8; - $actual = $prompt->terminal()->lines(); - - if ($actual < $required) { - throw new RuntimeException( - "The terminal height must be at least [$required] lines but is currently [$actual]. Please increase the height or reduce the font size." - ); - } - } }