-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Console] Modify console output and print multiple modifyable sections #24363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,10 @@ | |
|
||
namespace Symfony\Component\Console\Helper; | ||
|
||
use Symfony\Component\Console\Output\ConsoleSectionOutput; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Console\Exception\InvalidArgumentException; | ||
use Symfony\Component\Console\Exception\RuntimeException; | ||
|
||
/** | ||
* Provides helpers to display a table. | ||
|
@@ -70,6 +72,8 @@ class Table | |
|
||
private static $styles; | ||
|
||
private $rendered = false; | ||
|
||
public function __construct(OutputInterface $output) | ||
{ | ||
$this->output = $output; | ||
|
@@ -252,6 +256,25 @@ public function addRow($row) | |
return $this; | ||
} | ||
|
||
/** | ||
* Add a row to the table, and re-render the table. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adds |
||
*/ | ||
public function appendRow($row): self | ||
{ | ||
if (!$this->output instanceof ConsoleSectionOutput) { | ||
throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); | ||
} | ||
|
||
if ($this->rendered) { | ||
$this->output->clear($this->calculateRowCount()); | ||
} | ||
|
||
$this->addRow($row); | ||
$this->render(); | ||
|
||
return $this; | ||
} | ||
|
||
public function setRow($column, array $row) | ||
{ | ||
$this->rows[$column] = $row; | ||
|
@@ -311,6 +334,7 @@ public function render() | |
$this->renderRowSeparator(); | ||
|
||
$this->cleanup(); | ||
$this->rendered = true; | ||
} | ||
|
||
/** | ||
|
@@ -445,6 +469,19 @@ private function buildTableRows($rows) | |
}); | ||
} | ||
|
||
private function calculateRowCount(): int | ||
{ | ||
$numberOfRows = count(iterator_to_array($this->buildTableRows(array_merge($this->headers, array(new TableSeparator()), $this->rows)))); | ||
|
||
if ($this->headers) { | ||
++$numberOfRows; // Add row for header separator | ||
} | ||
|
||
++$numberOfRows; // Add row for footer separator | ||
|
||
return $numberOfRows; | ||
} | ||
|
||
/** | ||
* fill rows that contains rowspan > 1. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ | |
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface | ||
{ | ||
private $stderr; | ||
private $consoleSectionOutputs = array(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typing is missing, not only here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please document the content of that array |
||
|
||
/** | ||
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) | ||
|
@@ -48,6 +49,14 @@ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decor | |
} | ||
} | ||
|
||
/** | ||
* Creates a new output section. | ||
*/ | ||
public function section(): ConsoleSectionOutput | ||
{ | ||
return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Console\Output; | ||
|
||
use Symfony\Component\Console\Formatter\OutputFormatterInterface; | ||
use Symfony\Component\Console\Helper\Helper; | ||
use Symfony\Component\Console\Terminal; | ||
|
||
/** | ||
* @author Pierre du Plessis <pdples@gmail.com> | ||
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com> | ||
*/ | ||
class ConsoleSectionOutput extends StreamOutput | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this not be an output, taking the output as constructor arg instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be advantage of that when tradeoff is incompatible API? I would like to continue depending on OutputInterface with no changes. Client doesn't need to be aware that its output is going to appear in some section. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree with @ostrolucky here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it makes sense to use this class totally without if not, I would make this class internal and expose only interface of it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to construct this class completely without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this class should ever be used outside of ConsoleOutput, as it is an implementation detail of the console. I agree this class can be marked as internal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, using this feature as an end user involves to use the public api of this class, marking it as internal without providing an interface would be weird. I'm not sure we want to introduce an interface for that, so letting it as is (maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I have shown in different comment, there are situations when its a must to manipulate with order of sections manually, which can be done only when having access to array container. section() method provides only the most basic operation (creating new section at the bottom of the screen) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the thing. manually creating a section without having it anchored to full output class is weird and imo shall not be allowed. Thus section concrete class shall IMO be final, created only by output class itself, so interface to provide info how to use it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see anything weird about that. ConsoleOutput currently serves only as a simple stupid helper for creating new output sections at the bottom of the screen. I'm out of luck if I need to create another new section at the top of existing sections. Anyway I could currently do this even if that class is final, because as I said ConsoleOutput serves as a stupid helper only and I can bypass it, final or not. So I don't see what does it solve. I would need to ask symfony community to stop making final classes though, you cannot anticipate what will user space need to do. Marking it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree about all No, he didn't ;) he said it would be weird when done If you construct section out of the box without |
||
{ | ||
private $content = array(); | ||
private $lines = 0; | ||
private $sections; | ||
private $terminal; | ||
|
||
/** | ||
* @param resource $stream | ||
* @param ConsoleSectionOutput[] $sections | ||
*/ | ||
public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) | ||
{ | ||
parent::__construct($stream, $verbosity, $decorated, $formatter); | ||
array_unshift($sections, $this); | ||
$this->sections = &$sections; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why ref here? (here, when assigning to property, not when receiving input param) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a must so property is updated when different section appends itself into array |
||
$this->terminal = new Terminal(); | ||
} | ||
|
||
/** | ||
* Clears previous output for this section. | ||
* | ||
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared | ||
*/ | ||
public function clear(int $lines = null) | ||
{ | ||
if (empty($this->content) || !$this->isDecorated()) { | ||
return; | ||
} | ||
|
||
if ($lines) { | ||
\array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content | ||
} else { | ||
$lines = $this->lines; | ||
$this->content = array(); | ||
} | ||
|
||
$this->lines -= $lines; | ||
|
||
parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false); | ||
} | ||
|
||
/** | ||
* Overwrites the previous output with a new message. | ||
* | ||
* @param array|string $message | ||
*/ | ||
public function overwrite($message) | ||
{ | ||
$this->clear(); | ||
$this->writeln($message); | ||
} | ||
|
||
public function getContent(): string | ||
{ | ||
return implode('', $this->content); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function doWrite($message, $newline) | ||
{ | ||
if (!$this->isDecorated()) { | ||
return parent::doWrite($message, $newline); | ||
} | ||
|
||
$erasedContent = $this->popStreamContentUntilCurrentSection(); | ||
|
||
foreach (explode(PHP_EOL, $message) as $lineContent) { | ||
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1; | ||
$this->content[] = $lineContent; | ||
$this->content[] = PHP_EOL; | ||
} | ||
|
||
parent::doWrite($message, true); | ||
parent::doWrite($erasedContent, false); | ||
} | ||
|
||
/** | ||
* At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits | ||
* current section. Then it erases content it crawled through. Optionally, it erases part of current section too. | ||
*/ | ||
private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string | ||
{ | ||
$numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; | ||
$erasedContent = array(); | ||
|
||
foreach ($this->sections as $section) { | ||
if ($section === $this) { | ||
break; | ||
} | ||
|
||
$numberOfLinesToClear += $section->lines; | ||
$erasedContent[] = $section->getContent(); | ||
} | ||
|
||
if ($numberOfLinesToClear > 0) { | ||
// Move cursor up n lines | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move |
||
parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); | ||
// erase to end of screen | ||
parent::doWrite("\x1b[0J", false); | ||
} | ||
|
||
return implode('', array_reverse($erasedContent)); | ||
} | ||
|
||
private function getDisplayLength(string $text): string | ||
{ | ||
return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
modifiable