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 * @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 = $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 last chars are PHP_EOL 453 $this->bufferedOutput->write($message, $newLine, $type); 454 } 455 456 private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array 457 { 458 $indentLength = 0; 459 $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); 460 $lines = []; 461 462 if (null !== $type) { 463 $type = sprintf('[%s] ', $type); 464 $indentLength = \strlen($type); 465 $lineIndentation = str_repeat(' ', $indentLength); 466 } 467 468 // wrap and add newlines for each element 469 foreach ($messages as $key => $message) { 470 if ($escape) { 471 $message = OutputFormatter::escape($message); 472 } 473 474 $decorationLength = Helper::strlen($message) - Helper::strlenWithoutDecoration($this->getFormatter(), $message); 475 $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); 476 $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); 477 foreach ($messageLines as $messageLine) { 478 $lines[] = $messageLine; 479 } 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(' ', max($this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line), 0)); 500 501 if ($style) { 502 $line = sprintf('<%s>%s</>', $style, $line); 503 } 504 } 505 506 return $lines; 507 } 508} 509