Skip to content

Commit

Permalink
feature #19012 [Console] progress bar fix (TomasVotruba, fabpot)
Browse files Browse the repository at this point in the history
This PR was merged into the 3.2-dev branch.

Discussion
----------

[Console] progress bar fix

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #13019
| License       | MIT
| Doc PR        | -

This is #16490 where I've simplified the code as much as possible and added a test for the bug we're trying to fix.

The main change is the renaming of the `TerminalDimensionsProvider` to just `Terminal`. The new class can probably be useful to add more about the terminal.

Commits
-------

2f81247 switched to use COLUMNS and LINES env vars to change terminal dimensions
bf7a5c5 fixed logic
a589635 deprecated some Console Application methods
8f206c8 fixed CS, simplified code
b030c24 [Console] ProgressBar - adjust to the window width (static)
  • Loading branch information
fabpot committed Jun 17, 2016
2 parents 40f4c57 + 2f81247 commit afed2f8
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 137 deletions.
104 changes: 21 additions & 83 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -65,20 +65,19 @@ class Application
private $definition;
private $helperSet;
private $dispatcher;
private $terminalDimensions;
private $terminal;
private $defaultCommand;
private $singleCommand;

/**
* Constructor.
*
* @param string $name The name of the application
* @param string $version The version of the application
*/
public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
{
$this->name = $name;
$this->version = $version;
$this->terminal = new Terminal();
$this->defaultCommand = 'list';
$this->helperSet = $this->getDefaultHelperSet();
$this->definition = $this->getDefaultInputDefinition();
Expand Down Expand Up @@ -625,7 +624,7 @@ public function renderException(\Exception $e, OutputInterface $output)

$len = $this->stringWidth($title);

$width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX;
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
// HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327
if (defined('HHVM_VERSION') && $width > 1 << 31) {
$width = 1 << 31;
Expand Down Expand Up @@ -689,60 +688,42 @@ public function renderException(\Exception $e, OutputInterface $output)
* Tries to figure out the terminal width in which this application runs.
*
* @return int|null
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
protected function getTerminalWidth()
{
$dimensions = $this->getTerminalDimensions();
@trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__, ArgumentResolverInterface::class), E_USER_DEPRECATED);

return $dimensions[0];
return $this->terminal->getWidth();
}

/**
* Tries to figure out the terminal height in which this application runs.
*
* @return int|null
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
protected function getTerminalHeight()
{
$dimensions = $this->getTerminalDimensions();
@trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__, ArgumentResolverInterface::class), E_USER_DEPRECATED);

return $dimensions[1];
return $this->terminal->getHeight();
}

/**
* Tries to figure out the terminal dimensions based on the current environment.
*
* @return array Array containing width and height
*
* @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
*/
public function getTerminalDimensions()
{
if ($this->terminalDimensions) {
return $this->terminalDimensions;
}
@trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__, ArgumentResolverInterface::class), E_USER_DEPRECATED);

if ('\\' === DIRECTORY_SEPARATOR) {
// extract [w, H] from "wxh (WxH)"
if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) {
return array((int) $matches[1], (int) $matches[2]);
}
// extract [w, h] from "wxh"
if (preg_match('/^(\d+)x(\d+)$/', $this->getConsoleMode(), $matches)) {
return array((int) $matches[1], (int) $matches[2]);
}
}

if ($sttyString = $this->getSttyColumns()) {
// extract [w, h] from "rows h; columns w;"
if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
return array((int) $matches[2], (int) $matches[1]);
}
// extract [w, h] from "; h rows; w columns"
if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
return array((int) $matches[2], (int) $matches[1]);
}
}

return array(null, null);
return array($this->terminal->getWidth(), $this->terminal->getHeight());
}

/**
Expand All @@ -754,10 +735,15 @@ public function getTerminalDimensions()
* @param int $height The height
*
* @return Application The current application
*
* @deprecated since version 3.2, to be removed in 4.0. Set the COLUMNS and LINES env vars instead.
*/
public function setTerminalDimensions($width, $height)
{
$this->terminalDimensions = array($width, $height);
@trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Set the COLUMNS and LINES env vars instead.', __METHOD__, ArgumentResolverInterface::class), E_USER_DEPRECATED);

putenv('COLUMNS='.$width);
putenv('LINES='.$height);

return $this;
}
Expand Down Expand Up @@ -927,54 +913,6 @@ protected function getDefaultHelperSet()
));
}

/**
* Runs and parses stty -a if it's available, suppressing any error output.
*
* @return string
*/
private function getSttyColumns()
{
if (!function_exists('proc_open')) {
return;
}

$descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
$process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, array('suppress_errors' => true));
if (is_resource($process)) {
$info = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

return $info;
}
}

/**
* Runs and parses mode CON if it's available, suppressing any error output.
*
* @return string <width>x<height> or null if it could not be parsed
*/
private function getConsoleMode()
{
if (!function_exists('proc_open')) {
return;
}

$descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
$process = proc_open('mode CON', $descriptorspec, $pipes, null, null, array('suppress_errors' => true));
if (is_resource($process)) {
$info = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
return $matches[2].'x'.$matches[1];
}
}
}

/**
* Returns abbreviated suggestions in string format.
*
Expand Down
57 changes: 39 additions & 18 deletions src/Symfony/Component/Console/Helper/ProgressBar.php
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Terminal;

/**
* The ProgressBar provides helpers to display progress output.
Expand Down Expand Up @@ -44,13 +45,12 @@ class ProgressBar
private $formatLineCount;
private $messages = array();
private $overwrite = true;
private $terminal;

private static $formatters;
private static $formats;

/**
* Constructor.
*
* @param OutputInterface $output An OutputInterface instance
* @param int $max Maximum steps (0 if unknown)
*/
Expand All @@ -62,6 +62,7 @@ public function __construct(OutputInterface $output, $max = 0)

$this->output = $output;
$this->setMaxSteps($max);
$this->terminal = new Terminal();

if (!$this->output->isDecorated()) {
// disable overwrite when output does not support ANSI codes.
Expand Down Expand Up @@ -217,7 +218,7 @@ public function getProgressPercent()
*/
public function setBarWidth($size)
{
$this->barWidth = (int) $size;
$this->barWidth = max(1, (int) $size);
}

/**
Expand Down Expand Up @@ -412,21 +413,7 @@ public function display()
$this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
}

$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
$text = call_user_func($formatter, $this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
} else {
return $matches[0];
}

if (isset($matches[2])) {
$text = sprintf('%'.$matches[2], $text);
}

return $text;
}, $this->format));
$this->overwrite($this->buildLine());
}

/**
Expand Down Expand Up @@ -592,4 +579,38 @@ private static function initFormats()
'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
);
}

/**
* @return string
*/
private function buildLine()
{
$regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
$callback = function ($matches) {
if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
$text = call_user_func($formatter, $this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
} else {
return $matches[0];
}

if (isset($matches[2])) {
$text = sprintf('%'.$matches[2], $text);
}

return $text;
};
$line = preg_replace_callback($regex, $callback, $this->format);

$lineLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line);
$terminalWidth = $this->terminal->getWidth();
if ($lineLength <= $terminalWidth) {
return $line;
}

$this->setBarWidth($this->barWidth - $lineLength + $terminalWidth);

return preg_replace_callback($regex, $callback, $this->format);
}
}
13 changes: 3 additions & 10 deletions src/Symfony/Component/Console/Style/SymfonyStyle.php
Expand Up @@ -11,7 +11,6 @@

namespace Symfony\Component\Console\Style;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
Expand All @@ -25,6 +24,7 @@
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;

/**
* Output decorator helpers for the Symfony Style Guide.
Expand All @@ -50,7 +50,8 @@ public function __construct(InputInterface $input, OutputInterface $output)
$this->input = $input;
$this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
// Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
$this->lineLength = min($this->getTerminalWidth() - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);
$width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH;
$this->lineLength = min($width - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);

parent::__construct($output);
}
Expand Down Expand Up @@ -401,14 +402,6 @@ private function getProgressBar()
return $this->progressBar;
}

private function getTerminalWidth()
{
$application = new Application();
$dimensions = $application->getTerminalDimensions();

return $dimensions[0] ?: self::MAX_LINE_LENGTH;
}

private function autoPrependBlock()
{
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
Expand Down

0 comments on commit afed2f8

Please sign in to comment.