ConsoleSectionOutput.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  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 $content = [];
  21. private $lines = 0;
  22. private $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. * @param array|string $message
  58. */
  59. public function overwrite($message)
  60. {
  61. $this->clear();
  62. $this->writeln($message);
  63. }
  64. public function getContent(): string
  65. {
  66. return implode('', $this->content);
  67. }
  68. /**
  69. * @internal
  70. */
  71. public function addContent(string $input)
  72. {
  73. foreach (explode(PHP_EOL, $input) as $lineContent) {
  74. $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
  75. $this->content[] = $lineContent;
  76. $this->content[] = PHP_EOL;
  77. }
  78. }
  79. /**
  80. * {@inheritdoc}
  81. */
  82. protected function doWrite($message, $newline)
  83. {
  84. if (!$this->isDecorated()) {
  85. return parent::doWrite($message, $newline);
  86. }
  87. $erasedContent = $this->popStreamContentUntilCurrentSection();
  88. $this->addContent($message);
  89. parent::doWrite($message, true);
  90. parent::doWrite($erasedContent, false);
  91. }
  92. /**
  93. * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits
  94. * current section. Then it erases content it crawled through. Optionally, it erases part of current section too.
  95. */
  96. private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string
  97. {
  98. $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection;
  99. $erasedContent = [];
  100. foreach ($this->sections as $section) {
  101. if ($section === $this) {
  102. break;
  103. }
  104. $numberOfLinesToClear += $section->lines;
  105. $erasedContent[] = $section->getContent();
  106. }
  107. if ($numberOfLinesToClear > 0) {
  108. // move cursor up n lines
  109. parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
  110. // erase to end of screen
  111. parent::doWrite("\x1b[0J", false);
  112. }
  113. return implode('', array_reverse($erasedContent));
  114. }
  115. private function getDisplayLength(string $text): string
  116. {
  117. return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
  118. }
  119. }