Skip to content

Commit 660697a

Browse files
committed
[Console] fixed progress bar when using ANSI colors and Emojis
1 parent 6eb4c1e commit 660697a

File tree

4 files changed

+92
-41
lines changed

4 files changed

+92
-41
lines changed

Helper/Helper.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Console\Helper;
1313

14+
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
15+
1416
/**
1517
* Helper is the base class for all helper classes.
1618
*
@@ -103,4 +105,17 @@ public static function formatMemory($memory)
103105

104106
return sprintf('%d B', $memory);
105107
}
108+
109+
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
110+
{
111+
$isDecorated = $formatter->isDecorated();
112+
$formatter->setDecorated(false);
113+
// remove <...> formatting
114+
$string = $formatter->format($string);
115+
// remove already formatted characters
116+
$string = preg_replace("/\033\[[^m]*m/", '', $string);
117+
$formatter->setDecorated($isDecorated);
118+
119+
return self::strlen($string);
120+
}
106121
}

Helper/ProgressBar.php

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,15 @@ public function display()
387387
throw new \LogicException('You must start the progress bar before calling display().');
388388
}
389389

390+
// these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped.
390391
$self = $this;
391-
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) {
392+
$output = $this->output;
393+
$messages = $this->messages;
394+
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) {
392395
if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
393-
$text = call_user_func($formatter, $self);
394-
} elseif (isset($this->messages[$matches[1]])) {
395-
$text = $this->messages[$matches[1]];
396+
$text = call_user_func($formatter, $self, $output);
397+
} elseif (isset($messages[$matches[1]])) {
398+
$text = $messages[$matches[1]];
396399
} else {
397400
return $matches[0];
398401
}
@@ -424,23 +427,31 @@ public function clear()
424427
*/
425428
private function overwrite($message)
426429
{
427-
$length = Helper::strlen($message);
430+
$lines = explode("\n", $message);
428431

429-
// append whitespace to match the last line's length
430-
// FIXME: on each line!!!!!
431-
// FIXME: max of each line for lastMessagesLength or an array?
432-
if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) {
433-
$message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
432+
// append whitespace to match the line's length
433+
if (null !== $this->lastMessagesLength) {
434+
foreach ($lines as $i => $line) {
435+
if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) {
436+
$lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
437+
}
438+
}
434439
}
435440

436441
// move back to the beginning of the progress bar before redrawing it
437442
$this->output->write("\x0D");
438443
if ($this->formatLineCount) {
439444
$this->output->write(sprintf("\033[%dA", $this->formatLineCount));
440445
}
441-
$this->output->write($message);
446+
$this->output->write(implode("\n", $lines));
442447

443-
$this->lastMessagesLength = Helper::strlen($message);
448+
$this->lastMessagesLength = 0;
449+
foreach ($lines as $line) {
450+
$len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line);
451+
if ($len > $this->lastMessagesLength) {
452+
$this->lastMessagesLength = $len;
453+
}
454+
}
444455
}
445456

446457
private function determineBestFormat()
@@ -460,11 +471,11 @@ private function determineBestFormat()
460471
private static function initPlaceholderFormatters()
461472
{
462473
return array(
463-
'bar' => function (ProgressBar $bar) {
474+
'bar' => function (ProgressBar $bar, OutputInterface $output) {
464475
$completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth());
465-
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter());
466476
$display = str_repeat($bar->getBarCharacter(), $completeBars);
467477
if ($completeBars < $bar->getBarWidth()) {
478+
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
468479
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
469480
}
470481

Helper/TableHelper.php

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ private function renderCell(array $row, $column, $cellFormat)
427427
$width += strlen($cell) - mb_strlen($cell, $encoding);
428428
}
429429

430-
$width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell);
430+
$width += $this->strlen($cell) - self::strlenWithoutDecoration($this->output->getFormatter(), $cell);
431431

432432
$content = sprintf($this->cellRowContentFormat, $cell);
433433

@@ -486,7 +486,7 @@ private function getColumnWidth($column)
486486
*/
487487
private function getCellWidth(array $row, $column)
488488
{
489-
return isset($row[$column]) ? $this->computeLengthWithoutDecoration($row[$column]) : 0;
489+
return isset($row[$column]) ? self::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]) : 0;
490490
}
491491

492492
/**
@@ -498,18 +498,6 @@ private function cleanup()
498498
$this->numberOfColumns = null;
499499
}
500500

501-
private function computeLengthWithoutDecoration($string)
502-
{
503-
$formatter = $this->output->getFormatter();
504-
$isDecorated = $formatter->isDecorated();
505-
$formatter->setDecorated(false);
506-
507-
$string = $formatter->format($string);
508-
$formatter->setDecorated($isDecorated);
509-
510-
return $this->strlen($string);
511-
}
512-
513501
/**
514502
* {@inheritDoc}
515503
*/

Tests/Helper/ProgressBarTest.php

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Console\Tests\Helper;
1313

1414
use Symfony\Component\Console\Helper\ProgressBar;
15+
use Symfony\Component\Console\Helper\Helper;
1516
use Symfony\Component\Console\Output\StreamOutput;
1617

1718
class ProgressBarTest extends \PHPUnit_Framework_TestCase
@@ -206,7 +207,7 @@ public function testClear()
206207
$this->assertEquals(
207208
$this->generateOutput(' 0/50 [>---------------------------] 0%').
208209
$this->generateOutput(' 25/50 [==============>-------------] 50%').
209-
$this->generateOutput(''),
210+
$this->generateOutput(' '),
210211
stream_get_contents($output->getStream())
211212
);
212213
}
@@ -332,9 +333,53 @@ public function testMultilineFormat()
332333
rewind($output->getStream());
333334
$this->assertEquals(
334335
$this->generateOutput(">---------------------------\nfoobar").
335-
$this->generateOutput("=========>------------------\nfoobar").
336-
$this->generateOutput("\n").
337-
$this->generateOutput("============================\nfoobar"),
336+
$this->generateOutput("=========>------------------\nfoobar ").
337+
$this->generateOutput(" \n ").
338+
$this->generateOutput("============================\nfoobar "),
339+
stream_get_contents($output->getStream())
340+
);
341+
}
342+
343+
public function testAnsiColorsAndEmojis()
344+
{
345+
$bar = new ProgressBar($output = $this->getOutputStream(), 15);
346+
ProgressBar::setPlaceholderFormatterDefinition('memory', function (ProgressBar $bar) {
347+
static $i = 0;
348+
$mem = 100000 * $i;
349+
$colors = $i++ ? '41;37' : '44;37';
350+
351+
return "\033[".$colors."m ".Helper::formatMemory($mem)." \033[0m";
352+
});
353+
$bar->setFormat(" \033[44;37m %title:-37s% \033[0m\n %current%/%max% %bar% %percent:3s%%\n 🏁 %remaining:-10s% %memory:37s%");
354+
$bar->setBarCharacter($done = "\033[32m●\033[0m");
355+
$bar->setEmptyBarCharacter($empty = "\033[31m●\033[0m");
356+
$bar->setProgressCharacter($progress = "\033[32m➤ \033[0m");
357+
358+
$bar->setMessage('Starting the demo... fingers crossed', 'title');
359+
$bar->start();
360+
$bar->setMessage('Looks good to me...', 'title');
361+
$bar->advance(4);
362+
$bar->setMessage('Thanks, bye', 'title');
363+
$bar->finish();
364+
365+
rewind($output->getStream());
366+
367+
$this->assertEquals(
368+
$this->generateOutput(
369+
" \033[44;37m Starting the demo... fingers crossed \033[0m\n".
370+
" 0/15 ".$progress.str_repeat($empty, 26)." 0%\n".
371+
" 🏁 1 sec \033[44;37m 0 B \033[0m"
372+
).
373+
$this->generateOutput(
374+
" \033[44;37m Looks good to me... \033[0m\n".
375+
" 4/15 ".str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n".
376+
" 🏁 1 sec \033[41;37m 97 kB \033[0m"
377+
).
378+
$this->generateOutput(
379+
" \033[44;37m Thanks, bye \033[0m\n".
380+
" 15/15 ".str_repeat($done, 28)." 100%\n".
381+
" 🏁 1 sec \033[41;37m 195 kB \033[0m"
382+
),
338383
stream_get_contents($output->getStream())
339384
);
340385
}
@@ -346,16 +391,8 @@ protected function getOutputStream($decorated = true)
346391

347392
protected function generateOutput($expected)
348393
{
349-
$expectedout = $expected;
350-
351-
if (null !== $this->lastMessagesLength) {
352-
$expectedout = str_pad($expected, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
353-
}
354-
355-
$this->lastMessagesLength = strlen($expectedout);
356-
357394
$count = substr_count($expected, "\n");
358395

359-
return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expectedout;
396+
return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expected;
360397
}
361398
}

0 commit comments

Comments
 (0)