Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Console\Style; Chris@0: Chris@0: use Symfony\Component\Console\Exception\RuntimeException; Chris@0: use Symfony\Component\Console\Formatter\OutputFormatter; Chris@0: use Symfony\Component\Console\Helper\Helper; Chris@0: use Symfony\Component\Console\Helper\ProgressBar; Chris@0: use Symfony\Component\Console\Helper\SymfonyQuestionHelper; Chris@0: use Symfony\Component\Console\Helper\Table; Chris@0: use Symfony\Component\Console\Input\InputInterface; Chris@0: use Symfony\Component\Console\Output\BufferedOutput; Chris@0: use Symfony\Component\Console\Output\OutputInterface; Chris@0: use Symfony\Component\Console\Question\ChoiceQuestion; Chris@0: use Symfony\Component\Console\Question\ConfirmationQuestion; Chris@0: use Symfony\Component\Console\Question\Question; Chris@0: use Symfony\Component\Console\Terminal; Chris@0: Chris@0: /** Chris@0: * Output decorator helpers for the Symfony Style Guide. Chris@0: * Chris@0: * @author Kevin Bond Chris@0: */ Chris@0: class SymfonyStyle extends OutputStyle Chris@0: { Chris@0: const MAX_LINE_LENGTH = 120; Chris@0: Chris@0: private $input; Chris@0: private $questionHelper; Chris@0: private $progressBar; Chris@0: private $lineLength; Chris@0: private $bufferedOutput; Chris@0: Chris@0: public function __construct(InputInterface $input, OutputInterface $output) Chris@0: { Chris@0: $this->input = $input; Chris@0: $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); Chris@0: // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. Chris@0: $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; Chris@17: $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); Chris@0: Chris@0: parent::__construct($output); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Formats a message as a block of text. Chris@0: * Chris@0: * @param string|array $messages The message to write in the block Chris@0: * @param string|null $type The block type (added in [] on first line) Chris@0: * @param string|null $style The style to apply to the whole block Chris@0: * @param string $prefix The prefix for the block Chris@0: * @param bool $padding Whether to add vertical padding Chris@14: * @param bool $escape Whether to escape the message Chris@0: */ Chris@14: public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true) Chris@0: { Chris@17: $messages = \is_array($messages) ? array_values($messages) : [$messages]; Chris@0: Chris@0: $this->autoPrependBlock(); Chris@14: $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); Chris@0: $this->newLine(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function title($message) Chris@0: { Chris@0: $this->autoPrependBlock(); Chris@17: $this->writeln([ Chris@0: sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), Chris@0: sprintf('%s', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), Chris@17: ]); Chris@0: $this->newLine(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function section($message) Chris@0: { Chris@0: $this->autoPrependBlock(); Chris@17: $this->writeln([ Chris@0: sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), Chris@0: sprintf('%s', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), Chris@17: ]); Chris@0: $this->newLine(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function listing(array $elements) Chris@0: { Chris@0: $this->autoPrependText(); Chris@0: $elements = array_map(function ($element) { Chris@0: return sprintf(' * %s', $element); Chris@0: }, $elements); Chris@0: Chris@0: $this->writeln($elements); Chris@0: $this->newLine(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function text($message) Chris@0: { Chris@0: $this->autoPrependText(); Chris@0: Chris@17: $messages = \is_array($message) ? array_values($message) : [$message]; Chris@0: foreach ($messages as $message) { Chris@0: $this->writeln(sprintf(' %s', $message)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Formats a command comment. Chris@0: * Chris@0: * @param string|array $message Chris@0: */ Chris@0: public function comment($message) Chris@0: { Chris@14: $this->block($message, null, null, ' // ', false, false); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function success($message) Chris@0: { Chris@0: $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function error($message) Chris@0: { Chris@0: $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function warning($message) Chris@0: { Chris@0: $this->block($message, 'WARNING', 'fg=white;bg=red', ' ', true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function note($message) Chris@0: { Chris@0: $this->block($message, 'NOTE', 'fg=yellow', ' ! '); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function caution($message) Chris@0: { Chris@0: $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function table(array $headers, array $rows) Chris@0: { Chris@0: $style = clone Table::getStyleDefinition('symfony-style-guide'); Chris@0: $style->setCellHeaderFormat('%s'); Chris@0: Chris@0: $table = new Table($this); Chris@0: $table->setHeaders($headers); Chris@0: $table->setRows($rows); Chris@0: $table->setStyle($style); Chris@0: Chris@0: $table->render(); Chris@0: $this->newLine(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function ask($question, $default = null, $validator = null) Chris@0: { Chris@0: $question = new Question($question, $default); Chris@0: $question->setValidator($validator); Chris@0: Chris@0: return $this->askQuestion($question); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function askHidden($question, $validator = null) Chris@0: { Chris@0: $question = new Question($question); Chris@0: Chris@0: $question->setHidden(true); Chris@0: $question->setValidator($validator); Chris@0: Chris@0: return $this->askQuestion($question); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function confirm($question, $default = true) Chris@0: { Chris@0: return $this->askQuestion(new ConfirmationQuestion($question, $default)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function choice($question, array $choices, $default = null) Chris@0: { Chris@0: if (null !== $default) { Chris@0: $values = array_flip($choices); Chris@0: $default = $values[$default]; Chris@0: } Chris@0: Chris@0: return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function progressStart($max = 0) Chris@0: { Chris@0: $this->progressBar = $this->createProgressBar($max); Chris@0: $this->progressBar->start(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function progressAdvance($step = 1) Chris@0: { Chris@0: $this->getProgressBar()->advance($step); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function progressFinish() Chris@0: { Chris@0: $this->getProgressBar()->finish(); Chris@0: $this->newLine(2); Chris@0: $this->progressBar = null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createProgressBar($max = 0) Chris@0: { Chris@0: $progressBar = parent::createProgressBar($max); Chris@0: Chris@17: if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { Chris@0: $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 Chris@0: $progressBar->setProgressCharacter(''); Chris@0: $progressBar->setBarCharacter('▓'); // dark shade character \u2593 Chris@0: } Chris@0: Chris@0: return $progressBar; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @return mixed Chris@0: */ Chris@0: public function askQuestion(Question $question) Chris@0: { Chris@0: if ($this->input->isInteractive()) { Chris@0: $this->autoPrependBlock(); Chris@0: } Chris@0: Chris@0: if (!$this->questionHelper) { Chris@0: $this->questionHelper = new SymfonyQuestionHelper(); Chris@0: } Chris@0: Chris@0: $answer = $this->questionHelper->ask($this->input, $this, $question); Chris@0: Chris@0: if ($this->input->isInteractive()) { Chris@0: $this->newLine(); Chris@0: $this->bufferedOutput->write("\n"); Chris@0: } Chris@0: Chris@0: return $answer; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function writeln($messages, $type = self::OUTPUT_NORMAL) Chris@0: { Chris@0: parent::writeln($messages, $type); Chris@0: $this->bufferedOutput->writeln($this->reduceBuffer($messages), $type); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) Chris@0: { Chris@0: parent::write($messages, $newline, $type); Chris@0: $this->bufferedOutput->write($this->reduceBuffer($messages), $newline, $type); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function newLine($count = 1) Chris@0: { Chris@0: parent::newLine($count); Chris@0: $this->bufferedOutput->write(str_repeat("\n", $count)); Chris@0: } Chris@0: Chris@0: /** Chris@14: * Returns a new instance which makes use of stderr if available. Chris@14: * Chris@14: * @return self Chris@14: */ Chris@14: public function getErrorStyle() Chris@14: { Chris@14: return new self($this->input, $this->getErrorOutput()); Chris@14: } Chris@14: Chris@14: /** Chris@0: * @return ProgressBar Chris@0: */ Chris@0: private function getProgressBar() Chris@0: { Chris@0: if (!$this->progressBar) { Chris@0: throw new RuntimeException('The ProgressBar is not started.'); Chris@0: } Chris@0: Chris@0: return $this->progressBar; Chris@0: } Chris@0: Chris@0: private function autoPrependBlock() Chris@0: { Chris@0: $chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); Chris@0: Chris@0: if (!isset($chars[0])) { Chris@0: return $this->newLine(); //empty history, so we should start with a new line. Chris@0: } Chris@0: //Prepend new line for each non LF chars (This means no blank line was output before) Chris@0: $this->newLine(2 - substr_count($chars, "\n")); Chris@0: } Chris@0: Chris@0: private function autoPrependText() Chris@0: { Chris@0: $fetched = $this->bufferedOutput->fetch(); Chris@0: //Prepend new line if last char isn't EOL: Chris@0: if ("\n" !== substr($fetched, -1)) { Chris@0: $this->newLine(); Chris@0: } Chris@0: } Chris@0: Chris@0: private function reduceBuffer($messages) Chris@0: { Chris@0: // We need to know if the two last chars are PHP_EOL Chris@0: // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer Chris@0: return array_map(function ($value) { Chris@0: return substr($value, -4); Chris@17: }, array_merge([$this->bufferedOutput->fetch()], (array) $messages)); Chris@0: } Chris@0: Chris@0: private function createBlock($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = false) Chris@0: { Chris@0: $indentLength = 0; Chris@0: $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); Chris@17: $lines = []; Chris@0: Chris@0: if (null !== $type) { Chris@0: $type = sprintf('[%s] ', $type); Chris@17: $indentLength = \strlen($type); Chris@0: $lineIndentation = str_repeat(' ', $indentLength); Chris@0: } Chris@0: Chris@0: // wrap and add newlines for each element Chris@0: foreach ($messages as $key => $message) { Chris@0: if ($escape) { Chris@0: $message = OutputFormatter::escape($message); Chris@0: } Chris@0: Chris@0: $lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true))); Chris@0: Chris@17: if (\count($messages) > 1 && $key < \count($messages) - 1) { Chris@0: $lines[] = ''; Chris@0: } Chris@0: } Chris@0: Chris@0: $firstLineIndex = 0; Chris@0: if ($padding && $this->isDecorated()) { Chris@0: $firstLineIndex = 1; Chris@0: array_unshift($lines, ''); Chris@0: $lines[] = ''; Chris@0: } Chris@0: Chris@0: foreach ($lines as $i => &$line) { Chris@0: if (null !== $type) { Chris@0: $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; Chris@0: } Chris@0: Chris@0: $line = $prefix.$line; Chris@0: $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line)); Chris@0: Chris@0: if ($style) { Chris@0: $line = sprintf('<%s>%s', $style, $line); Chris@0: } Chris@0: } Chris@0: Chris@0: return $lines; Chris@0: } Chris@0: }