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
12namespace Symfony\Component\Console\Style;
13
14use Symfony\Component\Console\Exception\InvalidArgumentException;
15use Symfony\Component\Console\Exception\RuntimeException;
16use Symfony\Component\Console\Formatter\OutputFormatter;
17use Symfony\Component\Console\Helper\Helper;
18use Symfony\Component\Console\Helper\ProgressBar;
19use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
20use Symfony\Component\Console\Helper\Table;
21use Symfony\Component\Console\Helper\TableCell;
22use Symfony\Component\Console\Helper\TableSeparator;
23use Symfony\Component\Console\Input\InputInterface;
24use Symfony\Component\Console\Output\BufferedOutput;
25use Symfony\Component\Console\Output\OutputInterface;
26use Symfony\Component\Console\Question\ChoiceQuestion;
27use Symfony\Component\Console\Question\ConfirmationQuestion;
28use Symfony\Component\Console\Question\Question;
29use Symfony\Component\Console\Terminal;
30
31/**
32 * Output decorator helpers for the Symfony Style Guide.
33 *
34 * @author Kevin Bond <kevinbond@gmail.com>
35 */
36class SymfonyStyle extends OutputStyle
37{
38    const MAX_LINE_LENGTH = 120;
39
40    private $input;
41    private $questionHelper;
42    private $progressBar;
43    private $lineLength;
44    private $bufferedOutput;
45
46    public function __construct(InputInterface $input, OutputInterface $output)
47    {
48        $this->input = $input;
49        $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
50        // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
51        $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH;
52        $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);
53
54        parent::__construct($output);
55    }
56
57    /**
58     * Formats a message as a block of text.
59     *
60     * @param string|array $messages The message to write in the block
61     * @param string|null  $type     The block type (added in [] on first line)
62     * @param string|null  $style    The style to apply to the whole block
63     * @param string       $prefix   The prefix for the block
64     * @param bool         $padding  Whether to add vertical padding
65     * @param bool         $escape   Whether to escape the message
66     */
67    public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true)
68    {
69        $messages = \is_array($messages) ? array_values($messages) : [$messages];
70
71        $this->autoPrependBlock();
72        $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape));
73        $this->newLine();
74    }
75
76    /**
77     * {@inheritdoc}
78     */
79    public function title($message)
80    {
81        $this->autoPrependBlock();
82        $this->writeln([
83            sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
84            sprintf('<comment>%s</>', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
85        ]);
86        $this->newLine();
87    }
88
89    /**
90     * {@inheritdoc}
91     */
92    public function section($message)
93    {
94        $this->autoPrependBlock();
95        $this->writeln([
96            sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
97            sprintf('<comment>%s</>', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
98        ]);
99        $this->newLine();
100    }
101
102    /**
103     * {@inheritdoc}
104     */
105    public function listing(array $elements)
106    {
107        $this->autoPrependText();
108        $elements = array_map(function ($element) {
109            return sprintf(' * %s', $element);
110        }, $elements);
111
112        $this->writeln($elements);
113        $this->newLine();
114    }
115
116    /**
117     * {@inheritdoc}
118     */
119    public function text($message)
120    {
121        $this->autoPrependText();
122
123        $messages = \is_array($message) ? array_values($message) : [$message];
124        foreach ($messages as $message) {
125            $this->writeln(sprintf(' %s', $message));
126        }
127    }
128
129    /**
130     * Formats a command comment.
131     *
132     * @param string|array $message
133     */
134    public function comment($message)
135    {
136        $this->block($message, null, null, '<fg=default;bg=default> // </>', false, false);
137    }
138
139    /**
140     * {@inheritdoc}
141     */
142    public function success($message)
143    {
144        $this->block($message, 'OK', 'fg=black;bg=green', ' ', true);
145    }
146
147    /**
148     * {@inheritdoc}
149     */
150    public function error($message)
151    {
152        $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true);
153    }
154
155    /**
156     * {@inheritdoc}
157     */
158    public function warning($message)
159    {
160        $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true);
161    }
162
163    /**
164     * {@inheritdoc}
165     */
166    public function note($message)
167    {
168        $this->block($message, 'NOTE', 'fg=yellow', ' ! ');
169    }
170
171    /**
172     * {@inheritdoc}
173     */
174    public function caution($message)
175    {
176        $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true);
177    }
178
179    /**
180     * {@inheritdoc}
181     */
182    public function table(array $headers, array $rows)
183    {
184        $style = clone Table::getStyleDefinition('symfony-style-guide');
185        $style->setCellHeaderFormat('<info>%s</info>');
186
187        $table = new Table($this);
188        $table->setHeaders($headers);
189        $table->setRows($rows);
190        $table->setStyle($style);
191
192        $table->render();
193        $this->newLine();
194    }
195
196    /**
197     * Formats a horizontal table.
198     */
199    public function horizontalTable(array $headers, array $rows)
200    {
201        $style = clone Table::getStyleDefinition('symfony-style-guide');
202        $style->setCellHeaderFormat('<info>%s</info>');
203
204        $table = new Table($this);
205        $table->setHeaders($headers);
206        $table->setRows($rows);
207        $table->setStyle($style);
208        $table->setHorizontal(true);
209
210        $table->render();
211        $this->newLine();
212    }
213
214    /**
215     * Formats a list of key/value horizontally.
216     *
217     * Each row can be one of:
218     * * 'A title'
219     * * ['key' => 'value']
220     * * new TableSeparator()
221     *
222     * @param string|array|TableSeparator ...$list
223     */
224    public function definitionList(...$list)
225    {
226        $style = clone Table::getStyleDefinition('symfony-style-guide');
227        $style->setCellHeaderFormat('<info>%s</info>');
228
229        $table = new Table($this);
230        $headers = [];
231        $row = [];
232        foreach ($list as $value) {
233            if ($value instanceof TableSeparator) {
234                $headers[] = $value;
235                $row[] = $value;
236                continue;
237            }
238            if (\is_string($value)) {
239                $headers[] = new TableCell($value, ['colspan' => 2]);
240                $row[] = null;
241                continue;
242            }
243            if (!\is_array($value)) {
244                throw new InvalidArgumentException('Value should be an array, string, or an instance of TableSeparator.');
245            }
246            $headers[] = key($value);
247            $row[] = current($value);
248        }
249
250        $table->setHeaders($headers);
251        $table->setRows([$row]);
252        $table->setHorizontal();
253        $table->setStyle($style);
254
255        $table->render();
256        $this->newLine();
257    }
258
259    /**
260     * {@inheritdoc}
261     */
262    public function ask($question, $default = null, $validator = null)
263    {
264        $question = new Question($question, $default);
265        $question->setValidator($validator);
266
267        return $this->askQuestion($question);
268    }
269
270    /**
271     * {@inheritdoc}
272     */
273    public function askHidden($question, $validator = null)
274    {
275        $question = new Question($question);
276
277        $question->setHidden(true);
278        $question->setValidator($validator);
279
280        return $this->askQuestion($question);
281    }
282
283    /**
284     * {@inheritdoc}
285     */
286    public function confirm($question, $default = true)
287    {
288        return $this->askQuestion(new ConfirmationQuestion($question, $default));
289    }
290
291    /**
292     * {@inheritdoc}
293     */
294    public function choice($question, array $choices, $default = null)
295    {
296        if (null !== $default) {
297            $values = array_flip($choices);
298            $default = isset($values[$default]) ? $values[$default] : $default;
299        }
300
301        return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
302    }
303
304    /**
305     * {@inheritdoc}
306     */
307    public function progressStart($max = 0)
308    {
309        $this->progressBar = $this->createProgressBar($max);
310        $this->progressBar->start();
311    }
312
313    /**
314     * {@inheritdoc}
315     */
316    public function progressAdvance($step = 1)
317    {
318        $this->getProgressBar()->advance($step);
319    }
320
321    /**
322     * {@inheritdoc}
323     */
324    public function progressFinish()
325    {
326        $this->getProgressBar()->finish();
327        $this->newLine(2);
328        $this->progressBar = null;
329    }
330
331    /**
332     * {@inheritdoc}
333     */
334    public function createProgressBar($max = 0)
335    {
336        $progressBar = parent::createProgressBar($max);
337
338        if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) {
339            $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591
340            $progressBar->setProgressCharacter('');
341            $progressBar->setBarCharacter('▓'); // dark shade character \u2593
342        }
343
344        return $progressBar;
345    }
346
347    /**
348     * @return mixed
349     */
350    public function askQuestion(Question $question)
351    {
352        if ($this->input->isInteractive()) {
353            $this->autoPrependBlock();
354        }
355
356        if (!$this->questionHelper) {
357            $this->questionHelper = new SymfonyQuestionHelper();
358        }
359
360        $answer = $this->questionHelper->ask($this->input, $this, $question);
361
362        if ($this->input->isInteractive()) {
363            $this->newLine();
364            $this->bufferedOutput->write("\n");
365        }
366
367        return $answer;
368    }
369
370    /**
371     * {@inheritdoc}
372     */
373    public function writeln($messages, $type = self::OUTPUT_NORMAL)
374    {
375        if (!is_iterable($messages)) {
376            $messages = [$messages];
377        }
378
379        foreach ($messages as $message) {
380            parent::writeln($message, $type);
381            $this->writeBuffer($message, true, $type);
382        }
383    }
384
385    /**
386     * {@inheritdoc}
387     */
388    public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
389    {
390        if (!is_iterable($messages)) {
391            $messages = [$messages];
392        }
393
394        foreach ($messages as $message) {
395            parent::write($message, $newline, $type);
396            $this->writeBuffer($message, $newline, $type);
397        }
398    }
399
400    /**
401     * {@inheritdoc}
402     */
403    public function newLine($count = 1)
404    {
405        parent::newLine($count);
406        $this->bufferedOutput->write(str_repeat("\n", $count));
407    }
408
409    /**
410     * Returns a new instance which makes use of stderr if available.
411     *
412     * @return self
413     */
414    public function getErrorStyle()
415    {
416        return new self($this->input, $this->getErrorOutput());
417    }
418
419    private function getProgressBar(): ProgressBar
420    {
421        if (!$this->progressBar) {
422            throw new RuntimeException('The ProgressBar is not started.');
423        }
424
425        return $this->progressBar;
426    }
427
428    private function autoPrependBlock(): void
429    {
430        $chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
431
432        if (!isset($chars[0])) {
433            $this->newLine(); //empty history, so we should start with a new line.
434
435            return;
436        }
437        //Prepend new line for each non LF chars (This means no blank line was output before)
438        $this->newLine(2 - substr_count($chars, "\n"));
439    }
440
441    private function autoPrependText(): void
442    {
443        $fetched = $this->bufferedOutput->fetch();
444        //Prepend new line if last char isn't EOL:
445        if ("\n" !== substr($fetched, -1)) {
446            $this->newLine();
447        }
448    }
449
450    private function writeBuffer(string $message, bool $newLine, int $type): void
451    {
452        // We need to know if the two last chars are PHP_EOL
453        // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer
454        $this->bufferedOutput->write(substr($message, -4), $newLine, $type);
455    }
456
457    private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
458    {
459        $indentLength = 0;
460        $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix);
461        $lines = [];
462
463        if (null !== $type) {
464            $type = sprintf('[%s] ', $type);
465            $indentLength = \strlen($type);
466            $lineIndentation = str_repeat(' ', $indentLength);
467        }
468
469        // wrap and add newlines for each element
470        foreach ($messages as $key => $message) {
471            if ($escape) {
472                $message = OutputFormatter::escape($message);
473            }
474
475            $lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true)));
476
477            if (\count($messages) > 1 && $key < \count($messages) - 1) {
478                $lines[] = '';
479            }
480        }
481
482        $firstLineIndex = 0;
483        if ($padding && $this->isDecorated()) {
484            $firstLineIndex = 1;
485            array_unshift($lines, '');
486            $lines[] = '';
487        }
488
489        foreach ($lines as $i => &$line) {
490            if (null !== $type) {
491                $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line;
492            }
493
494            $line = $prefix.$line;
495            $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line));
496
497            if ($style) {
498                $line = sprintf('<%s>%s</>', $style, $line);
499            }
500        }
501
502        return $lines;
503    }
504}
505