Skip to content

Commit a72f98a

Browse files
committed
[Console] Add sections with a maximum height
1 parent c1f53b5 commit a72f98a

File tree

2 files changed

+103
-9
lines changed

2 files changed

+103
-9
lines changed

src/Symfony/Component/Console/Output/ConsoleSectionOutput.php

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class ConsoleSectionOutput extends StreamOutput
2525
private int $lines = 0;
2626
private array $sections;
2727
private Terminal $terminal;
28+
private int $maxHeight = 0;
2829

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

42+
/**
43+
* Defines a maximum number of lines for this section.
44+
*
45+
* When more lines are added, the section will automatically scroll to the
46+
* end (i.e. remove the first lines to comply with the max height).
47+
*/
48+
public function setMaxHeight(int $maxHeight): void
49+
{
50+
// when changing max height, clear output of current section and redraw again with the new height
51+
$existingContent = $this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $this->lines) : $this->lines);
52+
53+
$this->maxHeight = $maxHeight;
54+
55+
parent::doWrite($this->getVisibleContent(), false);
56+
parent::doWrite($existingContent, false);
57+
}
58+
4159
/**
4260
* Clears previous output for this section.
4361
*
@@ -58,7 +76,7 @@ public function clear(int $lines = null)
5876

5977
$this->lines -= $lines;
6078

61-
parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
79+
parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
6280
}
6381

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

96+
public function getVisibleContent(): string
97+
{
98+
if (0 === $this->maxHeight) {
99+
return $this->getContent();
100+
}
101+
102+
return implode('', \array_slice($this->content, -$this->maxHeight));
103+
}
104+
78105
/**
79106
* @internal
80107
*/
81-
public function addContent(string $input, bool $newline = true)
108+
public function addContent(string $input, bool $newline = true): int
82109
{
83110
$width = $this->terminal->getWidth();
84111
$lines = explode(\PHP_EOL, $input);
112+
$linesAdded = 0;
85113
$count = \count($lines) - 1;
86114
foreach ($lines as $i => $lineContent) {
87115
// re-add the line break (that has been removed in the above `explode()` for
@@ -113,8 +141,12 @@ public function addContent(string $input, bool $newline = true)
113141
$this->content[] = $lineContent;
114142
}
115143

116-
$this->lines += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
144+
$linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
117145
}
146+
147+
$this->lines += $linesAdded;
148+
149+
return $linesAdded;
118150
}
119151

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

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

133-
$this->addContent($message, $newline);
164+
$linesAdded = $this->addContent($message, $newline);
165+
166+
if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
167+
// on overflow, clear the whole section and redraw again (to remove the first lines)
168+
$linesToClear = $this->maxHeight;
169+
}
170+
171+
$erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);
172+
173+
if ($lineOverflow) {
174+
// redraw existing lines of the section
175+
$previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
176+
parent::doWrite(implode('', $previousLinesOfSection), false);
177+
}
134178

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

155199
$numberOfLinesToClear += $section->lines;
156-
if ('' !== $sectionContent = $section->getContent()) {
200+
if ('' !== $sectionContent = $section->getVisibleContent()) {
157201
if (!str_ends_with($sectionContent, \PHP_EOL)) {
158202
$sectionContent .= \PHP_EOL;
159203
}

src/Symfony/Component/Console/Tests/Output/ConsoleSectionOutputTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,56 @@ public function testOverwrite()
103103
$this->assertEquals('Foo'.\PHP_EOL."\x1b[1A\x1b[0JBar".\PHP_EOL, stream_get_contents($output->getStream()));
104104
}
105105

106+
public function testMaxHeight()
107+
{
108+
$expected = '';
109+
$sections = [];
110+
$output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
111+
$output->setMaxHeight(3);
112+
113+
// fill the section
114+
$output->writeln(['One', 'Two', 'Three']);
115+
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
116+
117+
// cause overflow (redraw whole section, without first line)
118+
$output->writeln('Four');
119+
$expected .= "\x1b[3A\x1b[0J";
120+
$expected .= 'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL;
121+
122+
// cause overflow with multiple new lines at once
123+
$output->writeln('Five'.\PHP_EOL.'Six');
124+
$expected .= "\x1b[3A\x1b[0J";
125+
$expected .= 'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;
126+
127+
// reset line height (redraw whole section, displaying all lines)
128+
$output->setMaxHeight(0);
129+
$expected .= "\x1b[3A\x1b[0J";
130+
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;
131+
132+
rewind($output->getStream());
133+
$this->assertEquals($expected, stream_get_contents($output->getStream()));
134+
}
135+
136+
public function testMaxHeightWithoutNewLine()
137+
{
138+
$expected = '';
139+
$sections = [];
140+
$output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
141+
$output->setMaxHeight(3);
142+
143+
// fill the section
144+
$output->writeln(['One', 'Two']);
145+
$output->write('Three');
146+
$expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
147+
148+
// append text to the last line
149+
$output->write(' and Four');
150+
$expected .= "\x1b[1A\x1b[0J".'Three and Four'.\PHP_EOL;
151+
152+
rewind($output->getStream());
153+
$this->assertEquals($expected, stream_get_contents($output->getStream()));
154+
}
155+
106156
public function testOverwriteMultipleLines()
107157
{
108158
$sections = [];

0 commit comments

Comments
 (0)