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