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