diff options
Diffstat (limited to 'vendor/symfony/console/Output/ConsoleSectionOutput.php')
-rw-r--r-- | vendor/symfony/console/Output/ConsoleSectionOutput.php | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/vendor/symfony/console/Output/ConsoleSectionOutput.php b/vendor/symfony/console/Output/ConsoleSectionOutput.php new file mode 100644 index 0000000..09aa7fe --- /dev/null +++ b/vendor/symfony/console/Output/ConsoleSectionOutput.php | |||
@@ -0,0 +1,237 @@ | |||
1 | <?php | ||
2 | |||
3 | /* | ||
4 | * This file is part of the Symfony package. | ||
5 | * | ||
6 | * (c) Fabien Potencier <fabien@symfony.com> | ||
7 | * | ||
8 | * For the full copyright and license information, please view the LICENSE | ||
9 | * file that was distributed with this source code. | ||
10 | */ | ||
11 | |||
12 | namespace Symfony\Component\Console\Output; | ||
13 | |||
14 | use Symfony\Component\Console\Formatter\OutputFormatterInterface; | ||
15 | use Symfony\Component\Console\Helper\Helper; | ||
16 | use Symfony\Component\Console\Terminal; | ||
17 | |||
18 | /** | ||
19 | * @author Pierre du Plessis <pdples@gmail.com> | ||
20 | * @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com> | ||
21 | */ | ||
22 | class ConsoleSectionOutput extends StreamOutput | ||
23 | { | ||
24 | private array $content = []; | ||
25 | private int $lines = 0; | ||
26 | private array $sections; | ||
27 | private Terminal $terminal; | ||
28 | private int $maxHeight = 0; | ||
29 | |||
30 | /** | ||
31 | * @param resource $stream | ||
32 | * @param ConsoleSectionOutput[] $sections | ||
33 | */ | ||
34 | public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) | ||
35 | { | ||
36 | parent::__construct($stream, $verbosity, $decorated, $formatter); | ||
37 | array_unshift($sections, $this); | ||
38 | $this->sections = &$sections; | ||
39 | $this->terminal = new Terminal(); | ||
40 | } | ||
41 | |||
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 | $previousMaxHeight = $this->maxHeight; | ||
52 | $this->maxHeight = $maxHeight; | ||
53 | $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines); | ||
54 | |||
55 | parent::doWrite($this->getVisibleContent(), false); | ||
56 | parent::doWrite($existingContent, false); | ||
57 | } | ||
58 | |||
59 | /** | ||
60 | * Clears previous output for this section. | ||
61 | * | ||
62 | * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared | ||
63 | */ | ||
64 | public function clear(?int $lines = null): void | ||
65 | { | ||
66 | if (!$this->content || !$this->isDecorated()) { | ||
67 | return; | ||
68 | } | ||
69 | |||
70 | if ($lines) { | ||
71 | array_splice($this->content, -$lines); | ||
72 | } else { | ||
73 | $lines = $this->lines; | ||
74 | $this->content = []; | ||
75 | } | ||
76 | |||
77 | $this->lines -= $lines; | ||
78 | |||
79 | parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false); | ||
80 | } | ||
81 | |||
82 | /** | ||
83 | * Overwrites the previous output with a new message. | ||
84 | */ | ||
85 | public function overwrite(string|iterable $message): void | ||
86 | { | ||
87 | $this->clear(); | ||
88 | $this->writeln($message); | ||
89 | } | ||
90 | |||
91 | public function getContent(): string | ||
92 | { | ||
93 | return implode('', $this->content); | ||
94 | } | ||
95 | |||
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 | |||
105 | /** | ||
106 | * @internal | ||
107 | */ | ||
108 | public function addContent(string $input, bool $newline = true): int | ||
109 | { | ||
110 | $width = $this->terminal->getWidth(); | ||
111 | $lines = explode(\PHP_EOL, $input); | ||
112 | $linesAdded = 0; | ||
113 | $count = \count($lines) - 1; | ||
114 | foreach ($lines as $i => $lineContent) { | ||
115 | // re-add the line break (that has been removed in the above `explode()` for | ||
116 | // - every line that is not the last line | ||
117 | // - if $newline is required, also add it to the last line | ||
118 | if ($i < $count || $newline) { | ||
119 | $lineContent .= \PHP_EOL; | ||
120 | } | ||
121 | |||
122 | // skip line if there is no text (or newline for that matter) | ||
123 | if ('' === $lineContent) { | ||
124 | continue; | ||
125 | } | ||
126 | |||
127 | // For the first line, check if the previous line (last entry of `$this->content`) | ||
128 | // needs to be continued (i.e. does not end with a line break). | ||
129 | if (0 === $i | ||
130 | && (false !== $lastLine = end($this->content)) | ||
131 | && !str_ends_with($lastLine, \PHP_EOL) | ||
132 | ) { | ||
133 | // deduct the line count of the previous line | ||
134 | $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1; | ||
135 | // concatenate previous and new line | ||
136 | $lineContent = $lastLine.$lineContent; | ||
137 | // replace last entry of `$this->content` with the new expanded line | ||
138 | array_splice($this->content, -1, 1, $lineContent); | ||
139 | } else { | ||
140 | // otherwise just add the new content | ||
141 | $this->content[] = $lineContent; | ||
142 | } | ||
143 | |||
144 | $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1; | ||
145 | } | ||
146 | |||
147 | $this->lines += $linesAdded; | ||
148 | |||
149 | return $linesAdded; | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * @internal | ||
154 | */ | ||
155 | public function addNewLineOfInputSubmit(): void | ||
156 | { | ||
157 | $this->content[] = \PHP_EOL; | ||
158 | ++$this->lines; | ||
159 | } | ||
160 | |||
161 | protected function doWrite(string $message, bool $newline): void | ||
162 | { | ||
163 | // Simulate newline behavior for consistent output formatting, avoiding extra logic | ||
164 | if (!$newline && str_ends_with($message, \PHP_EOL)) { | ||
165 | $message = substr($message, 0, -\strlen(\PHP_EOL)); | ||
166 | $newline = true; | ||
167 | } | ||
168 | |||
169 | if (!$this->isDecorated()) { | ||
170 | parent::doWrite($message, $newline); | ||
171 | |||
172 | return; | ||
173 | } | ||
174 | |||
175 | // Check if the previous line (last entry of `$this->content`) needs to be continued | ||
176 | // (i.e. does not end with a line break). In which case, it needs to be erased first. | ||
177 | $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0; | ||
178 | |||
179 | $linesAdded = $this->addContent($message, $newline); | ||
180 | |||
181 | if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) { | ||
182 | // on overflow, clear the whole section and redraw again (to remove the first lines) | ||
183 | $linesToClear = $this->maxHeight; | ||
184 | } | ||
185 | |||
186 | $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear); | ||
187 | |||
188 | if ($lineOverflow) { | ||
189 | // redraw existing lines of the section | ||
190 | $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded); | ||
191 | parent::doWrite(implode('', $previousLinesOfSection), false); | ||
192 | } | ||
193 | |||
194 | // if the last line was removed, re-print its content together with the new content. | ||
195 | // otherwise, just print the new content. | ||
196 | parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true); | ||
197 | parent::doWrite($erasedContent, false); | ||
198 | } | ||
199 | |||
200 | /** | ||
201 | * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits | ||
202 | * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. | ||
203 | */ | ||
204 | private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string | ||
205 | { | ||
206 | $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; | ||
207 | $erasedContent = []; | ||
208 | |||
209 | foreach ($this->sections as $section) { | ||
210 | if ($section === $this) { | ||
211 | break; | ||
212 | } | ||
213 | |||
214 | $numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines; | ||
215 | if ('' !== $sectionContent = $section->getVisibleContent()) { | ||
216 | if (!str_ends_with($sectionContent, \PHP_EOL)) { | ||
217 | $sectionContent .= \PHP_EOL; | ||
218 | } | ||
219 | $erasedContent[] = $sectionContent; | ||
220 | } | ||
221 | } | ||
222 | |||
223 | if ($numberOfLinesToClear > 0) { | ||
224 | // move cursor up n lines | ||
225 | parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); | ||
226 | // erase to end of screen | ||
227 | parent::doWrite("\x1b[0J", false); | ||
228 | } | ||
229 | |||
230 | return implode('', array_reverse($erasedContent)); | ||
231 | } | ||
232 | |||
233 | private function getDisplayLength(string $text): int | ||
234 | { | ||
235 | return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); | ||
236 | } | ||
237 | } | ||