Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions src/Symfony/Component/Console/Output/ConsoleSectionOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ConsoleSectionOutput extends StreamOutput
private int $lines = 0;
private array $sections;
private Terminal $terminal;
private int $maxHeight = 0;

/**
* @param resource $stream
Expand All @@ -38,6 +39,23 @@ public function __construct($stream, array &$sections, int $verbosity, bool $dec
$this->terminal = new Terminal();
}

/**
* Defines a maximum number of lines for this section.
*
* When more lines are added, the section will automatically scroll to the
* end (i.e. remove the first lines to comply with the max height).
*/
public function setMaxHeight(int $maxHeight): void
{
// when changing max height, clear output of current section and redraw again with the new height
$existingContent = $this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $this->lines) : $this->lines);

$this->maxHeight = $maxHeight;

parent::doWrite($this->getVisibleContent(), false);
parent::doWrite($existingContent, false);
}

/**
* Clears previous output for this section.
*
Expand All @@ -58,7 +76,7 @@ public function clear(int $lines = null)

$this->lines -= $lines;

parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
}

/**
Expand All @@ -75,13 +93,23 @@ public function getContent(): string
return implode('', $this->content);
}

public function getVisibleContent(): string
{
if (0 === $this->maxHeight) {
return $this->getContent();
}

return implode('', \array_slice($this->content, -$this->maxHeight));
}

/**
* @internal
*/
public function addContent(string $input, bool $newline = true)
public function addContent(string $input, bool $newline = true): int
{
$width = $this->terminal->getWidth();
$lines = explode(\PHP_EOL, $input);
$linesAdded = 0;
$count = \count($lines) - 1;
foreach ($lines as $i => $lineContent) {
// re-add the line break (that has been removed in the above `explode()` for
Expand Down Expand Up @@ -113,8 +141,12 @@ public function addContent(string $input, bool $newline = true)
$this->content[] = $lineContent;
}

$this->lines += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
$linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
}

$this->lines += $linesAdded;

return $linesAdded;
}

protected function doWrite(string $message, bool $newline)
Expand All @@ -127,13 +159,25 @@ protected function doWrite(string $message, bool $newline)

// Check if the previous line (last entry of `$this->content`) needs to be continued
// (i.e. does not end with a line break). In which case, it needs to be erased first.
$deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;
$erasedContent = $this->popStreamContentUntilCurrentSection($deleteLastLine);
$linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;

$this->addContent($message, $newline);
$linesAdded = $this->addContent($message, $newline);

if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
// on overflow, clear the whole section and redraw again (to remove the first lines)
$linesToClear = $this->maxHeight;
}

$erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);

if ($lineOverflow) {
// redraw existing lines of the section
$previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
parent::doWrite(implode('', $previousLinesOfSection), false);
}

// If the last line was removed, re-print its content together with the new content.
// Otherwise, just print the new content.
// if the last line was removed, re-print its content together with the new content.
// otherwise, just print the new content.
parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true);
parent::doWrite($erasedContent, false);
}
Expand All @@ -153,7 +197,7 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr
}

$numberOfLinesToClear += $section->lines;
if ('' !== $sectionContent = $section->getContent()) {
if ('' !== $sectionContent = $section->getVisibleContent()) {
if (!str_ends_with($sectionContent, \PHP_EOL)) {
$sectionContent .= \PHP_EOL;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,56 @@ public function testOverwrite()
$this->assertEquals('Foo'.\PHP_EOL."\x1b[1A\x1b[0JBar".\PHP_EOL, stream_get_contents($output->getStream()));
}

public function testMaxHeight()
{
$expected = '';
$sections = [];
$output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
$output->setMaxHeight(3);

// fill the section
$output->writeln(['One', 'Two', 'Three']);
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;

// cause overflow (redraw whole section, without first line)
$output->writeln('Four');
$expected .= "\x1b[3A\x1b[0J";
$expected .= 'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL;

// cause overflow with multiple new lines at once
$output->writeln('Five'.\PHP_EOL.'Six');
$expected .= "\x1b[3A\x1b[0J";
$expected .= 'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;

// reset line height (redraw whole section, displaying all lines)
$output->setMaxHeight(0);
$expected .= "\x1b[3A\x1b[0J";
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;

rewind($output->getStream());
$this->assertEquals($expected, stream_get_contents($output->getStream()));
}

public function testMaxHeightWithoutNewLine()
{
$expected = '';
$sections = [];
$output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
$output->setMaxHeight(3);

// fill the section
$output->writeln(['One', 'Two']);
$output->write('Three');
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;

// append text to the last line
$output->write(' and Four');
$expected .= "\x1b[1A\x1b[0J".'Three and Four'.\PHP_EOL;

rewind($output->getStream());
$this->assertEquals($expected, stream_get_contents($output->getStream()));
}

public function testOverwriteMultipleLines()
{
$sections = [];
Expand Down