[Console] fixed progress bar when using ANSI colors and Emojis

This commit is contained in:
Fabien Potencier 2014-03-01 22:26:36 +01:00
parent 38f7a6f817
commit 8c0022b769
4 changed files with 92 additions and 41 deletions

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
/**
* Helper is the base class for all helper classes.
*
@ -103,4 +105,17 @@ abstract class Helper implements HelperInterface
return sprintf('%d B', $memory);
}
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
{
$isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false);
// remove <...> formatting
$string = $formatter->format($string);
// remove already formatted characters
$string = preg_replace("/\033\[[^m]*m/", '', $string);
$formatter->setDecorated($isDecorated);
return self::strlen($string);
}
}

View File

@ -387,12 +387,15 @@ class ProgressBar
throw new \LogicException('You must start the progress bar before calling display().');
}
// these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped.
$self = $this;
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) {
$output = $this->output;
$messages = $this->messages;
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) {
if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
$text = call_user_func($formatter, $self);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
$text = call_user_func($formatter, $self, $output);
} elseif (isset($messages[$matches[1]])) {
$text = $messages[$matches[1]];
} else {
return $matches[0];
}
@ -424,13 +427,15 @@ class ProgressBar
*/
private function overwrite($message)
{
$length = Helper::strlen($message);
$lines = explode("\n", $message);
// append whitespace to match the last line's length
// FIXME: on each line!!!!!
// FIXME: max of each line for lastMessagesLength or an array?
if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) {
$message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
// append whitespace to match the line's length
if (null !== $this->lastMessagesLength) {
foreach ($lines as $i => $line) {
if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) {
$lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
}
}
}
// move back to the beginning of the progress bar before redrawing it
@ -438,9 +443,15 @@ class ProgressBar
if ($this->formatLineCount) {
$this->output->write(sprintf("\033[%dA", $this->formatLineCount));
}
$this->output->write($message);
$this->output->write(implode("\n", $lines));
$this->lastMessagesLength = Helper::strlen($message);
$this->lastMessagesLength = 0;
foreach ($lines as $line) {
$len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line);
if ($len > $this->lastMessagesLength) {
$this->lastMessagesLength = $len;
}
}
}
private function determineBestFormat()
@ -460,11 +471,11 @@ class ProgressBar
private static function initPlaceholderFormatters()
{
return array(
'bar' => function (ProgressBar $bar) {
'bar' => function (ProgressBar $bar, OutputInterface $output) {
$completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth());
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter());
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
}

View File

@ -427,7 +427,7 @@ class TableHelper extends Helper
$width += strlen($cell) - mb_strlen($cell, $encoding);
}
$width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell);
$width += $this->strlen($cell) - self::strlenWithoutDecoration($this->output->getFormatter(), $cell);
$content = sprintf($this->cellRowContentFormat, $cell);
@ -486,7 +486,7 @@ class TableHelper extends Helper
*/
private function getCellWidth(array $row, $column)
{
return isset($row[$column]) ? $this->computeLengthWithoutDecoration($row[$column]) : 0;
return isset($row[$column]) ? self::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]) : 0;
}
/**
@ -498,18 +498,6 @@ class TableHelper extends Helper
$this->numberOfColumns = null;
}
private function computeLengthWithoutDecoration($string)
{
$formatter = $this->output->getFormatter();
$isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false);
$string = $formatter->format($string);
$formatter->setDecorated($isDecorated);
return $this->strlen($string);
}
/**
* {@inheritDoc}
*/

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Tests\Helper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Output\StreamOutput;
class ProgressBarTest extends \PHPUnit_Framework_TestCase
@ -206,7 +207,7 @@ class ProgressBarTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(
$this->generateOutput(' 0/50 [>---------------------------] 0%').
$this->generateOutput(' 25/50 [==============>-------------] 50%').
$this->generateOutput(''),
$this->generateOutput(' '),
stream_get_contents($output->getStream())
);
}
@ -332,9 +333,53 @@ class ProgressBarTest extends \PHPUnit_Framework_TestCase
rewind($output->getStream());
$this->assertEquals(
$this->generateOutput(">---------------------------\nfoobar").
$this->generateOutput("=========>------------------\nfoobar").
$this->generateOutput("\n").
$this->generateOutput("============================\nfoobar"),
$this->generateOutput("=========>------------------\nfoobar ").
$this->generateOutput(" \n ").
$this->generateOutput("============================\nfoobar "),
stream_get_contents($output->getStream())
);
}
public function testAnsiColorsAndEmojis()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 15);
ProgressBar::setPlaceholderFormatterDefinition('memory', function (ProgressBar $bar) {
static $i = 0;
$mem = 100000 * $i;
$colors = $i++ ? '41;37' : '44;37';
return "\033[".$colors."m ".Helper::formatMemory($mem)." \033[0m";
});
$bar->setFormat(" \033[44;37m %title:-37s% \033[0m\n %current%/%max% %bar% %percent:3s%%\n 🏁 %remaining:-10s% %memory:37s%");
$bar->setBarCharacter($done = "\033[32m●\033[0m");
$bar->setEmptyBarCharacter($empty = "\033[31m●\033[0m");
$bar->setProgressCharacter($progress = "\033[32m➤ \033[0m");
$bar->setMessage('Starting the demo... fingers crossed', 'title');
$bar->start();
$bar->setMessage('Looks good to me...', 'title');
$bar->advance(4);
$bar->setMessage('Thanks, bye', 'title');
$bar->finish();
rewind($output->getStream());
$this->assertEquals(
$this->generateOutput(
" \033[44;37m Starting the demo... fingers crossed \033[0m\n".
" 0/15 ".$progress.str_repeat($empty, 26)." 0%\n".
" 🏁 1 sec \033[44;37m 0 B \033[0m"
).
$this->generateOutput(
" \033[44;37m Looks good to me... \033[0m\n".
" 4/15 ".str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n".
" 🏁 1 sec \033[41;37m 97 kB \033[0m"
).
$this->generateOutput(
" \033[44;37m Thanks, bye \033[0m\n".
" 15/15 ".str_repeat($done, 28)." 100%\n".
" 🏁 1 sec \033[41;37m 195 kB \033[0m"
),
stream_get_contents($output->getStream())
);
}
@ -346,16 +391,8 @@ class ProgressBarTest extends \PHPUnit_Framework_TestCase
protected function generateOutput($expected)
{
$expectedout = $expected;
if (null !== $this->lastMessagesLength) {
$expectedout = str_pad($expected, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
}
$this->lastMessagesLength = strlen($expectedout);
$count = substr_count($expected, "\n");
return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expectedout;
return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expected;
}
}