-
-
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 all 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 |
---|---|---|
@@ -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. 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; | ||
$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->isDe EED3 corated()) { | ||
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 | ||
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.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
agree with @ostrolucky here
let say I want to make app with 2 sections (top, bottom), and then I sent given sections as an input for some subcommands. They don't need to know if they run on full output or just section of it.