ConsoleSectionOutput.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Output;
  11. use Symfony\Component\Console\Formatter\OutputFormatterInterface;
  12. use Symfony\Component\Console\Helper\Helper;
  13. use Symfony\Component\Console\Terminal;
  14. /**
  15. * @author Pierre du Plessis <pdples@gmail.com>
  16. * @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
  17. */
  18. class ConsoleSectionOutput extends StreamOutput
  19. {
  20. private array $content = [];
  21. private int $lines = 0;
  22. private array $sections;
  23. private $terminal;
  24. /**
  25. * @param resource $stream
  26. * @param ConsoleSectionOutput[] $sections
  27. */
  28. public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter)
  29. {
  30. parent::__construct($stream, $verbosity, $decorated, $formatter);
  31. array_unshift($sections, $this);
  32. $this->sections = &$sections;
  33. $this->terminal = new Terminal();
  34. }
  35. /**
  36. * Clears previous output for this section.
  37. *
  38. * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
  39. */
  40. public function clear(int $lines = null)
  41. {
  42. if (empty($this->content) || !$this->isDecorated()) {
  43. return;
  44. }
  45. if ($lines) {
  46. array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
  47. } else {
  48. $lines = $this->lines;
  49. $this->content = [];
  50. }
  51. $this->lines -= $lines;
  52. parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
  53. }
  54. /**
  55. * Overwrites the previous output with a new message.
  56. */
  57. public function overwrite(string|iterable $message)
  58. {
  59. $this->clear();
  60. $this->writeln($message);
  61. }
  62. public function getContent(): string
  63. {
  64. return implode('', $this->content);
  65. }
  66. /**
  67. * @internal
  68. */
  69. public function addContent(string $input)
  70. {
  71. foreach (explode(\PHP_EOL, $input) as $lineContent) {
  72. $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
  73. $this->content[] = $lineContent;
  74. $this->content[] = \PHP_EOL;
  75. }
  76. }
  77. /**
  78. * {@inheritdoc}
  79. */
  80. protected function doWrite(string $message, bool $newline)
  81. {
  82. if (!$this->isDecorated()) {
  83. parent::doWrite($message, $newline);
  84. return;
  85. }
  86. $erasedContent = $this->popStreamContentUntilCurrentSection();
  87. $this->addContent($message);
  88. parent::doWrite($message, true);
  89. parent::doWrite($erasedContent, false);
  90. }
  91. /**
  92. * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
  93. * current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
  94. */
  95. private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
  96. {
  97. $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
  98. $erasedContent = [];
  99. foreach ($this->sections as $section) {
  100. if ($section === $this) {
  101. break;
  102. }
  103. $numberOfLinesToClear += $section->lines;
  104. $erasedContent[] = $section->getContent();
  105. }
  106. if ($numberOfLinesToClear > 0) {
  107. // move cursor up n lines
  108. parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
  109. // erase to end of screen
  110. parent::doWrite("\x1b[0J", false);
  111. }
  112. return implode('', array_reverse($erasedContent));
  113. }
  114. private function getDisplayLength(string $text): int
  115. {
  116. return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)));
  117. }
  118. }