Skip to content

Commit eb1a92c

Browse files
committed
add console formatter support for bright 3/4bit and all 8bit colors
- enables namespaces colors, including: - normal: 4-bit normal colors - bright: 4-bit bright colors - mode : format "[0-9]" for 8-bit color or format "[0-9]+,[0-9]+,[0-9]+" for 24-bit RGB color - examples of supported foreground/background colors: - green - bright:blue - mode:178 - mode:20,0,100 - examples of supported format markup and resulting output: - "<bg=bright-blue;>[str]</>" -> "\033[104m[str]\033[49m"1 - "<fg=bright-green;options=bold>[str]</>" -> "\033[92;1m[str]\033[39;22m" - "<fg=mode-54;bg=mode-253>[str]</>" -> "\033[38;5;54;48;5;253m[str]\033[39;49m" - "<fg=mode-7;bg=bright:red;options=reverse>[str]</>" -> "\033[38;5;7;101m[str]\033[39;49m" - "<fg=mode-0,255,0;bg=bright-cyan>[str]</>" -> "[38;2;0;255;0;106m[test][39;49m"
1 parent 5cf0a2e commit eb1a92c

File tree

3 files changed

+312
-56
lines changed

3 files changed

+312
-56
lines changed

src/Symfony/Component/Console/Formatter/OutputFormatterStyle.php

Lines changed: 229 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,49 @@
1717
* Formatter style class for defining styles.
1818
*
1919
* @author Konstantin Kudryashov <ever.zet@gmail.com>
20+
* @author Rob Frawley 2nd <rmf@src.run>
2021
*/
2122
class OutputFormatterStyle implements OutputFormatterStyleInterface
2223
{
23-
private static $availableForegroundColors = array(
24-
'black' => array('set' => 30, 'unset' => 39),
25-
'red' => array('set' => 31, 'unset' => 39),
26-
'green' => array('set' => 32, 'unset' => 39),
27-
'yellow' => array('set' => 33, 'unset' => 39),
28-
'blue' => array('set' => 34, 'unset' => 39),
29-
'magenta' => array('set' => 35, 'unset' => 39),
30-
'cyan' => array('set' => 36, 'unset' => 39),
31-
'white' => array('set' => 37, 'unset' => 39),
32-
'default' => array('set' => 39, 'unset' => 39),
24+
private const FOREGROUND_COLOR_FORMATS = array(
25+
'04-bit-normal' => '3%d',
26+
'04-bit-bright' => '9%d',
27+
'08-bit' => '38;5;%d',
28+
'24-bit' => '38;2;%d;%d;%d',
3329
);
34-
private static $availableBackgroundColors = array(
35-
'black' => array('set' => 40, 'unset' => 49),
36-
'red' => array('set' => 41, 'unset' => 49),
37-
'green' => array('set' => 42, 'unset' => 49),
38-
'yellow' => array('set' => 43, 'unset' => 49),
39-
'blue' => array('set' => 44, 'unset' => 49),
40-
'magenta' => array('set' => 45, 'unset' => 49),
41-
'cyan' => array('set' => 46, 'unset' => 49),
42-
'white' => array('set' => 47, 'unset' => 49),
43-
'default' => array('set' => 49, 'unset' => 49),
30+
private const FOREGROUND_COLOR_RANGE = array(0, 255);
31+
private const FOREGROUND_COLOR_UNSET = 39;
32+
private const FOREGROUND_COLOR_NAMES = array(
33+
'black' => 0,
34+
'red' => 1,
35+
'green' => 2,
36+
'yellow' => 3,
37+
'blue' => 4,
38+
'magenta' => 5,
39+
'cyan' => 6,
40+
'white' => 7,
41+
'default' => 9,
4442
);
45-
private static $availableOptions = array(
43+
private const BACKGROUND_COLOR_FORMATS = array(
44+
'04-bit-normal' => '4%d',
45+
'04-bit-bright' => '10%d',
46+
'08-bit' => '48;5;%d',
47+
'24-bit' => '48;2;%d;%d;%d',
48+
);
49+
private const BACKGROUND_COLOR_RANGE = array(0, 255);
50+
private const BACKGROUND_COLOR_UNSET = 49;
51+
private const BACKGROUND_COLOR_NAMES = array(
52+
'black' => 0,
53+
'red' => 1,
54+
'green' => 2,
55+
'yellow' => 3,
56+
'blue' => 4,
57+
'magenta' => 5,
58+
'cyan' => 6,
59+
'white' => 7,
60+
'default' => 9,
61+
);
62+
private const FORMATTING_OPTIONS = array(
4663
'bold' => array('set' => 1, 'unset' => 22),
4764
'underscore' => array('set' => 4, 'unset' => 24),
4865
'blink' => array('set' => 5, 'unset' => 25),
@@ -89,15 +106,11 @@ public function setForeground($color = null)
89106
return;
90107
}
91108

92-
if (!isset(static::$availableForegroundColors[$color])) {
93-
throw new InvalidArgumentException(sprintf(
94-
'Invalid foreground color specified: "%s". Expected one of (%s)',
95-
$color,
96-
implode(', ', array_keys(static::$availableForegroundColors))
97-
));
109+
if (null === $definition = self::buildColorDefinition($color, 'foreground')) {
110+
throw new InvalidArgumentException(self::getInputColorExcMessage($color, 'foreground'));
98111
}
99112

100-
$this->foreground = static::$availableForegroundColors[$color];
113+
$this->foreground = $definition;
101114
}
102115

103116
/**
@@ -115,15 +128,11 @@ public function setBackground($color = null)
115128
return;
116129
}
117130

118-
if (!isset(static::$availableBackgroundColors[$color])) {
119-
throw new InvalidArgumentException(sprintf(
120-
'Invalid background color specified: "%s". Expected one of (%s)',
121-
$color,
122-
implode(', ', array_keys(static::$availableBackgroundColors))
123-
));
131+
if (null === $definition = self::buildColorDefinition($color, 'background')) {
132+
throw new InvalidArgumentException(self::getInputColorExcMessage($color, 'background'));
124133
}
125134

126-
$this->background = static::$availableBackgroundColors[$color];
135+
$this->background = $definition;
127136
}
128137

129138
/**
@@ -135,16 +144,12 @@ public function setBackground($color = null)
135144
*/
136145
public function setOption($option)
137146
{
138-
if (!isset(static::$availableOptions[$option])) {
139-
throw new InvalidArgumentException(sprintf(
140-
'Invalid option specified: "%s". Expected one of (%s)',
141-
$option,
142-
implode(', ', array_keys(static::$availableOptions))
143-
));
147+
if (!isset(self::FORMATTING_OPTIONS[$option])) {
148+
throw new InvalidArgumentException(self::getInputOptionExcMessage($option));
144149
}
145150

146-
if (!in_array(static::$availableOptions[$option], $this->options)) {
147-
$this->options[] = static::$availableOptions[$option];
151+
if (!in_array(self::FORMATTING_OPTIONS[$option], $this->options)) {
152+
$this->options[] = self::FORMATTING_OPTIONS[$option];
148153
}
149154
}
150155

@@ -157,15 +162,11 @@ public function setOption($option)
157162
*/
158163
public function unsetOption($option)
159164
{
160-
if (!isset(static::$availableOptions[$option])) {
161-
throw new InvalidArgumentException(sprintf(
162-
'Invalid option specified: "%s". Expected one of (%s)',
163-
$option,
164-
implode(', ', array_keys(static::$availableOptions))
165-
));
165+
if (!isset(self::FORMATTING_OPTIONS[$option])) {
166+
throw new InvalidArgumentException(self::getInputOptionExcMessage($option));
166167
}
167168

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

217218
return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
218219
}
220+
221+
/**
222+
* @param string $input
223+
* @param string $context
224+
*
225+
* @return array|null
226+
*/
227+
private static function buildColorDefinition(string $input, string $context): ?array
228+
{
229+
if (null !== $inst = self::createColorDef24Bit($input)) {
230+
return self::finalizeColorDefinition($inst, $context);
231+
}
232+
233+
if (null !== $inst = self::createColorDef08Bit($input, self::getColorConstForContext($context, 'range'))) {
234+
return self::finalizeColorDefinition($inst, $context);
235+
}
236+
237+
if (null !== $inst = self::createColorDef04Bit($input, self::getColorConstForContext($context, 'names'))) {
238+
return self::finalizeColorDefinition($inst, $context);
239+
}
240+
241+
return null;
242+
}
243+
244+
/**
245+
* @param string $color
246+
*
247+
* @return array|null
248+
*/
249+
private static function createColorDef24Bit(string $color): ?array
250+
{
251+
$match = self::matchColorStr('/^mode\-(?<r>[0-9]+),(?<b>[0-9]+),(?<g>[0-9]+)$/', $color);
252+
$level = function (int $coordinate): bool {
253+
return $coordinate >= 0 && $coordinate <= 255;
254+
};
255+
256+
if (null !== $match && $level($match['r']) && $level($match['b']) && $level($match['g'])) {
257+
return array(
258+
'type' => '24-bit',
259+
'args' => array($match['r'], $match['b'], $match['g']),
260+
);
261+
}
262+
263+
return null;
264+
}
265+
266+
/**
267+
* @param string $color
268+
* @param array $range
269+
*
270+
* @return array|null
271+
*/
272+
private static function createColorDef08Bit(string $color, array $range): ?array
273+
{
274+
$match = self::matchColorStr('/^mode\-(?<name>[0-9]+)$/', $color);
275+
276+
if (null !== $match && $match['name'] >= $range[0] && $match['name'] <= $range[1]) {
277+
return array(
278+
'type' => '08-bit',
279+
'args' => array($match['name']),
280+
);
281+
}
282+
283+
return null;
284+
}
285+
286+
/**
287+
* @param string $color
288+
* @param array $allow
289+
*
290+
* @return array|null
291+
*/
292+
private static function createColorDef04Bit(string $color, array $allow): ?array
293+
{
294+
$match = self::matchColorStr('/^(?<type>normal|bright)\-(?<name>[a-z]+)$/', $color);
295+
296+
if (null !== $match && isset($allow[$match['name']])) {
297+
return array(
298+
'type' => sprintf('04-bit-%s', $match['type']),
299+
'args' => array($allow[$match['name']]),
300+
);
301+
}
302+
303+
if (isset($allow[$color])) {
304+
return array(
305+
'type' => '04-bit-normal',
306+
'args' => array($allow[$color]),
307+
);
308+
}
309+
310+
return null;
311+
}
312+
313+
/**
314+
* @param array $partial
315+
* @param string $context
316+
*
317+
* @return array
318+
*/
319+
private static function finalizeColorDefinition(array $partial, string $context)
320+
{
321+
return $partial + array(
322+
'unset' => self::getColorConstForContext($context, 'unset'),
323+
'set' => vsprintf(
324+
self::getColorConstForContext($context, 'formats')[$partial['type']], $partial['args']
325+
),
326+
);
327+
}
328+
329+
/**
330+
* @param string $regex
331+
* @param string $color
332+
*
333+
* @return array|null
334+
*/
335+
private static function matchColorStr(string $regex, string $color): ?array
336+
{
337+
if (1 === preg_match($regex, $color, $match)) {
338+
return $match;
339+
}
340+
341+
return null;
342+
}
343+
344+
/**
345+
* @param string $color
346+
* @param string $context
347+
*
348+
* @return string
349+
*/
350+
private static function getInputColorExcMessage(string $color, string $context): string
351+
{
352+
$format = 'Invalid %s color "%s" specified. Accepted colors include 24-bit colors as "mode-<int>,<int>,<int>" '
353+
.'(RGB), 8-bit colors as "mode-<int>" (0-255), and 4-bit colors as "<string>", "default-<name>", or '
354+
.'"bright-<name>". Accepted 4-bit color names: %s.';
355+
356+
return vsprintf($format, array(
357+
$context,
358+
$color,
359+
self::getFormattedArrayKeysFlattened(self::getColorConstForContext($context, 'names')),
360+
));
361+
}
362+
363+
/**
364+
* @param string $option
365+
*
366+
* @return string
367+
*/
368+
private static function getInputOptionExcMessage(string $option): string
369+
{
370+
return vsprintf('Invalid option specified: "%s". Accepted options names: %s.', array(
371+
$option,
372+
self::getFormattedArrayKeysFlattened(self::FORMATTING_OPTIONS),
373+
));
374+
}
375+
376+
/**
377+
* @param string[] $array
378+
*
379+
* @return string
380+
*/
381+
private static function getFormattedArrayKeysFlattened(array $array): string
382+
{
383+
return implode(', ', array_map(function (string $string): string {
384+
return sprintf('"%s"', $string);
385+
}, array_keys($array)));
386+
}
387+
388+
/**
389+
* @param string $context
390+
* @param string $name
391+
*
392+
* @return mixed
393+
*/
394+
private static function getColorConstForContext(string $context, string $name)
395+
{
396+
return constant(strtoupper(sprintf('self::%s_COLOR_%s', $context, $name)));
397+
}
219398
}

0 commit comments

Comments
 (0)