From c862a07d1f95c778b8e537ed6119763bbe607688 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 4 Jun 2026 09:58:24 +0100 Subject: [PATCH 1/2] feature: buffer lines to output repeat_char even on `write` commands closes #268 --- src/Command/Command.php | 6 +- src/Stream.php | 74 ++++++++++++++++------ test/phpunit/Command/CommandOutputTest.php | 20 +++++- test/phpunit/StreamTest.php | 36 +++++++++++ 4 files changed, 115 insertions(+), 21 deletions(-) diff --git a/src/Command/Command.php b/src/Command/Command.php index 2e4a79f..b137fef 100644 --- a/src/Command/Command.php +++ b/src/Command/Command.php @@ -339,7 +339,11 @@ protected function writeLine( string $message = "", StreamName $streamName = StreamName::OUT ):void { - $this->write($message . PHP_EOL, $streamName); + if(!isset($this->stream)) { + return; + } + + $this->stream->writeLine($message, $streamName); } protected function readLine(?string $default = null):string { diff --git a/src/Stream.php b/src/Stream.php index d342028..ebbc0f0 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -18,6 +18,8 @@ class Stream { protected string $lastLineBuffer; private bool $lastLineRepeats; + /** @var array */ + private array $lineBuffer; public Cursor $cursor; public function __construct( @@ -39,6 +41,7 @@ public function __construct( $this->setStream($in, $out, $error); $this->lastLineBuffer = ""; $this->lastLineRepeats = false; + $this->lineBuffer = []; } public function setStream(string $in, string $out, string $error):void { @@ -106,32 +109,33 @@ public function writeLine( ?Palette $foreground = null, ?Palette $background = null, ):void { - $line = $message . PHP_EOL; + $this->writeCompleteLine( + $message . PHP_EOL, + $streamName, + $foreground, + $background, + ); + } - if($line === $this->lastLineBuffer) { - $this->write( - self::REPEAT_CHAR, - $streamName, - $foreground, - $background - ); - $this->lastLineRepeats = true; - } - else { - if($this->lastLineRepeats) { - $this->write(PHP_EOL, $streamName); - } + public function writeBufferedLines( + string $message, + StreamName $streamName = StreamName::OUT, + ?Palette $foreground = null, + ?Palette $background = null, + ):void { + $this->lineBuffer[$streamName->value] ??= ""; + $this->lineBuffer[$streamName->value] .= $message; - $this->write( + while(($newlinePos = strpos($this->lineBuffer[$streamName->value], "\n")) !== false) { + $line = substr($this->lineBuffer[$streamName->value], 0, $newlinePos + 1); + $this->lineBuffer[$streamName->value] = substr($this->lineBuffer[$streamName->value], $newlinePos + 1); + $this->writeCompleteLine( $line, $streamName, $foreground, - $background + $background, ); - $this->lastLineRepeats = false; } - - $this->lastLineBuffer = $line; } public function setOutputPalette( @@ -181,4 +185,36 @@ private function wrapInPalette( . $message . self::ANSI_RESET; } + + private function writeCompleteLine( + string $line, + StreamName $streamName, + ?Palette $foreground = null, + ?Palette $background = null, + ):void { + if($line === $this->lastLineBuffer) { + $this->write( + self::REPEAT_CHAR, + $streamName, + $foreground, + $background + ); + $this->lastLineRepeats = true; + } + else { + if($this->lastLineRepeats) { + $this->write(PHP_EOL, $streamName); + } + + $this->write( + $line, + $streamName, + $foreground, + $background + ); + $this->lastLineRepeats = false; + } + + $this->lastLineBuffer = $line; + } } diff --git a/test/phpunit/Command/CommandOutputTest.php b/test/phpunit/Command/CommandOutputTest.php index 4db55eb..a84a73c 100644 --- a/test/phpunit/Command/CommandOutputTest.php +++ b/test/phpunit/Command/CommandOutputTest.php @@ -22,7 +22,7 @@ public function testSetOutput():void { StreamName::OUT->value => [], StreamName::ERROR->value => [], ]; - $stream->method("write") + $stream->method("writeLine") ->willReturnCallback(function( string $message, StreamName $streamName @@ -60,6 +60,24 @@ public function outputPublic(string $message, ?Palette $colour = null):void { $command->outputPublic("single green message", Palette::GREEN); } + public function testWriteLineUsesStreamWriteLine():void { + $stream = $this->createMock(Stream::class); + $stream->expects(self::once()) + ->method("writeLine") + ->with( + "line output", + StreamName::OUT + ); + + $command = new class extends TestCommand { + public function writeLinePublic(string $message):void { + $this->writeLine($message); + } + }; + $command->setStream($stream); + $command->writeLinePublic("line output"); + } + public function testSetAndResetOutputPalette():void { $stream = $this->createMock(Stream::class); $stream->expects(self::once()) diff --git a/test/phpunit/StreamTest.php b/test/phpunit/StreamTest.php index 52dfae4..4068941 100644 --- a/test/phpunit/StreamTest.php +++ b/test/phpunit/StreamTest.php @@ -172,6 +172,42 @@ public function testRepeatingLineSuppressed():void { self::assertSame(4, substr_count($fullStreamContents, Stream::REPEAT_CHAR)); } + public function testBufferedRepeatingLinesSuppressed():void { + $stream = new Stream( + "php://memory", + "php://memory", + "php://memory", + ); + $out = $stream->getOutStream(); + + $stream->writeBufferedLines("repeat"); + $stream->writeBufferedLines(" me\nrepeat me\n"); + $stream->writeBufferedLines("repeat me\nunique\n"); + + $out->rewind(); + self::assertSame( + "repeat me\n" . Stream::REPEAT_CHAR . Stream::REPEAT_CHAR . "\nunique\n", + $out->fread(1024) + ); + } + + public function testBufferedWritesHoldPartialLinesUntilNewline():void { + $stream = new Stream( + "php://memory", + "php://memory", + "php://memory", + ); + $out = $stream->getOutStream(); + + $stream->writeBufferedLines("partial"); + $out->rewind(); + self::assertSame("", $out->fread(1024)); + + $stream->writeBufferedLines(" line\n"); + $out->rewind(); + self::assertSame("partial line\n", $out->fread(1024)); + } + public function testWriteLineWithTemporaryPalette() { $stream = new Stream( "php://memory", From 87a50e8c190934c279aca67db7ef7eb0862ebd1e Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Thu, 4 Jun 2026 10:29:50 +0100 Subject: [PATCH 2/2] tidy: refactor complex loop --- src/Stream.php | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Stream.php b/src/Stream.php index ebbc0f0..ec5bc29 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -123,12 +123,26 @@ public function writeBufferedLines( ?Palette $foreground = null, ?Palette $background = null, ):void { - $this->lineBuffer[$streamName->value] ??= ""; - $this->lineBuffer[$streamName->value] .= $message; + $bufferKey = $streamName->value; + $this->lineBuffer[$bufferKey] ??= ""; + $this->lineBuffer[$bufferKey] .= $message; + + while(true) { + $newlinePos = strpos($this->lineBuffer[$bufferKey], "\n"); + if($newlinePos === false) { + break; + } - while(($newlinePos = strpos($this->lineBuffer[$streamName->value], "\n")) !== false) { - $line = substr($this->lineBuffer[$streamName->value], 0, $newlinePos + 1); - $this->lineBuffer[$streamName->value] = substr($this->lineBuffer[$streamName->value], $newlinePos + 1); + $line = substr( + $this->lineBuffer[$bufferKey], + 0, + $newlinePos + 1, + ); + $remainingBufferOffset = $newlinePos + 1; + $this->lineBuffer[$bufferKey] = substr( + $this->lineBuffer[$bufferKey], + $remainingBufferOffset, + ); $this->writeCompleteLine( $line, $streamName,