Skip to content

[WIP] [Console] Support "named", "ansi", "hex", and "rgb" color support to console formatter #26576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 229 additions & 50 deletions src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,49 @@
* Formatter style class for defining styles.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
* @author Rob Frawley 2nd <rmf@src.run>
*/
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
private static $availableForegroundColors = array(
'black' => array('set' => 30, 'unset' => 39),
'red' => array('set' => 31, 'unset' => 39),
'green' => array('set' => 32, 'unset' => 39),
'yellow' => array('set' => 33, 'unset' => 39),
'blue' => array('set' => 34, 'unset' => 39),
'magenta' => array('set' => 35, 'unset' => 39),
'cyan' => array('set' => 36, 'unset' => 39),
'white' => array('set' => 37, 'unset' => 39),
'default' => array('set' => 39, 'unset' => 39),
private const FOREGROUND_COLOR_FORMATS = array(
'04-bit-normal' => '3%d',
'04-bit-bright' => '9%d',
'08-bit' => '38;5;%d',
'24-bit' => '38;2;%d;%d;%d',
);
private static $availableBackgroundColors = array(
'black' => array('set' => 40, 'unset' => 49),
'red' => array('set' => 41, 'unset' => 49),
'green' => array('set' => 42, 'unset' => 49),
'yellow' => array('set' => 43, 'unset' => 49),
'blue' => array('set' => 44, 'unset' => 49),
'magenta' => array('set' => 45, 'unset' => 49),
'cyan' => array('set' => 46, 'unset' => 49),
'white' => array('set' => 47, 'unset' => 49),
'default' => array('set' => 49, 'unset' => 49),
private const FOREGROUND_COLOR_RANGE = array(0, 255);
private const FOREGROUND_COLOR_UNSET = 39;
private const FOREGROUND_COLOR_NAMES = array(
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
);
private static $availableOptions = array(
private const BACKGROUND_COLOR_FORMATS = array(
'04-bit-normal' => '4%d',
'04-bit-bright' => '10%d',
'08-bit' => '48;5;%d',
'24-bit' => '48;2;%d;%d;%d',
);
private const BACKGROUND_COLOR_RANGE = array(0, 255);
private const BACKGROUND_COLOR_UNSET = 49;
private const BACKGROUND_COLOR_NAMES = array(
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
);
private const FORMATTING_OPTIONS = array(
'bold' => array('set' => 1, 'unset' => 22),
'underscore' => array('set' => 4, 'unset' => 24),
'blink' => array('set' => 5, 'unset' => 25),
Expand Down Expand Up @@ -89,15 +106,11 @@ public function setForeground($color = null)
return;
}

if (!isset(static::$availableForegroundColors[$color])) {
throw new InvalidArgumentException(sprintf(
'Invalid foreground color specified: "%s". Expected one of (%s)',
$color,
implode(', ', array_keys(static::$availableForegroundColors))
));
if (null === $definition = self::buildColorDefinition($color, 'foreground')) {
throw new InvalidArgumentException(self::getInputColorExcMessage($color, 'foreground'));
}

$this->foreground = static::$availableForegroundColors[$color];
$this->foreground = $definition;
}

/**
Expand All @@ -115,15 +128,11 @@ public function setBackground($color = null)
return;
}

if (!isset(static::$availableBackgroundColors[$color])) {
throw new InvalidArgumentException(sprintf(
'Invalid background color specified: "%s". Expected one of (%s)',
$color,
implode(', ', array_keys(static::$availableBackgroundColors))
));
if (null === $definition = self::buildColorDefinition($color, 'background')) {
throw new InvalidArgumentException(self::getInputColorExcMessage($color, 'background'));
}

$this->background = static::$availableBackgroundColors[$color];
$this->background = $definition;
}

/**
Expand All @@ -135,16 +144,12 @@ public function setBackground($color = null)
*/
public function setOption($option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf(
'Invalid option specified: "%s". Expected one of (%s)',
$option,
implode(', ', array_keys(static::$availableOptions))
));
if (!isset(self::FORMATTING_OPTIONS[$option])) {
throw new InvalidArgumentException(self::getInputOptionExcMessage($option));
}

if (!in_array(static::$availableOptions[$option], $this->options)) {
$this->options[] = static::$availableOptions[$option];
if (!in_array(self::FORMATTING_OPTIONS[$option], $this->options)) {
$this->options[] = self::FORMATTING_OPTIONS[$option];
}
}

Expand All @@ -157,15 +162,11 @@ public function setOption($option)
*/
public function unsetOption($option)
{
if (!isset(static::$availableOptions[$option])) {
throw new InvalidArgumentException(sprintf(
'Invalid option specified: "%s". Expected one of (%s)',
$option,
implode(', ', array_keys(static::$availableOptions))
));
if (!isset(self::FORMATTING_OPTIONS[$option])) {
throw new InvalidArgumentException(self::getInputOptionExcMessage($option));
}

$pos = array_search(static::$availableOptions[$option], $this->options);
$pos = array_search(self::FORMATTING_OPTIONS[$option], $this->options);
if (false !== $pos) {
unset($this->options[$pos]);
}
Expand Down Expand Up @@ -216,4 +217,182 @@ public function apply($text)

return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
}

/**
* @param string $input
* @param string $context
*
* @return array|null
*/
private static function buildColorDefinition(string $input, string $context): ?array
{
if (null !== $inst = self::createColorDef24Bit($input)) {
return self::finalizeColorDefinition($inst, $context);
}

if (null !== $inst = self::createColorDef08Bit($input, self::getColorConstForContext($context, 'range'))) {
return self::finalizeColorDefinition($inst, $context);
}

if (null !== $inst = self::createColorDef04Bit($input, self::getColorConstForContext($context, 'names'))) {
return self::finalizeColorDefinition($inst, $context);
}

return null;
}

/**
* @param string $color
*
* @return array|null
*/
private static function createColorDef24Bit(string $color): ?array
{
$match = self::matchColorStr('/^mode\-(?<r>[0-9]+),(?<b>[0-9]+),(?<g>[0-9]+)$/', $color);
$level = function (int $coordinate): bool {
return $coordinate >= 0 && $coordinate <= 255;
};

if (null !== $match && $level($match['r']) && $level($match['b']) && $level($match['g'])) {
return array(
'type' => '24-bit',
'args' => array($match['r'], $match['b'], $match['g']),
);
}

return null;
}

/**
* @param string $color
* @param array $range
*
* @return array|null
*/
private static function createColorDef08Bit(string $color, array $range): ?array
{
$match = self::matchColorStr('/^mode\-(?<name>[0-9]+)$/', $color);

if (null !== $match && $match['name'] >= $range[0] && $match['name'] <= $range[1]) {
return array(
'type' => '08-bit',
'args' => array($match['name']),
);
}

return null;
}

/**
* @param string $color
* @param array $allow
*
* @return array|null
*/
private static function createColorDef04Bit(string $color, array $allow): ?array
{
$match = self::matchColorStr('/^(?<type>normal|bright)\-(?<name>[a-z]+)$/', $color);

if (null !== $match && isset($allow[$match['name']])) {
return array(
'type' => sprintf('04-bit-%s', $match['type']),
'args' => array($allow[$match['name']]),
);
}

if (isset($allow[$color])) {
return array(
'type' => '04-bit-normal',
'args' => array($allow[$color]),
);
}

return null;
}

/**
* @param array $partial
* @param string $context
*
* @return array
*/
private static function finalizeColorDefinition(array $partial, string $context)
{
return $partial + array(
'unset' => self::getColorConstForContext($context, 'unset'),
'set' => vsprintf(
self::getColorConstForContext($context, 'formats')[$partial['type']], $partial['args']
),
);
}

/**
* @param string $regex
* @param string $color
*
* @return array|null
*/
private static function matchColorStr(string $regex, string $color): ?array
{
if (1 === preg_match($regex, $color, $match)) {
return $match;
}

return null;
}

/**
* @param string $color
* @param string $context
*
* @return string
*/
private static function getInputColorExcMessage(string $color, string $context): string
{
$format = 'Invalid %s color "%s" specified. Accepted colors include 24-bit colors as "mode-<int>,<int>,<int>" '
.'(RGB), 8-bit colors as "mode-<int>" (0-255), and 4-bit colors as "<string>", "default-<name>", or '
.'"bright-<name>". Accepted 4-bit color names: %s.';

return vsprintf($format, array(
$context,
$color,
self::getFormattedArrayKeysFlattened(self::getColorConstForContext($context, 'names')),
));
}

/**
* @param string $option
*
* @return string
*/
private static function getInputOptionExcMessage(string $option): string
{
return vsprintf('Invalid option specified: "%s". Accepted options names: %s.', array(
$option,
self::getFormattedArrayKeysFlattened(self::FORMATTING_OPTIONS),
));
}

/**
* @param string[] $array
*
* @return string
*/
private static function getFormattedArrayKeysFlattened(array $array): string
{
return implode(', ', array_map(function (string $string): string {
return sprintf('"%s"', $string);
}, array_keys($array)));
}

/**
* @param string $context
* @param string $name
*
* @return mixed
*/
private static function getColorConstForContext(string $context, string $name)
{
return constant(strtoupper(sprintf('self::%s_COLOR_%s', $context, $name)));
}
}
Loading