Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6c8c93d

Browse files
committed
feature #46944 [Console] Add Ansi8 (256 color) support, improve true color (Ansi24) support detection (julien-boudry)
This PR was merged into the 6.2 branch. Discussion ---------- [Console] Add Ansi8 (256 color) support, improve true color (Ansi24) support detection | Q | A | ------------- | --- | Branch? | 6.2 for features | Bug fix? | no | New feature? | yes | Deprecations? |no | License | MIT * Refactor the detection code of the colorimetric capacities of the terminal (much easier to extend next) * Improve true color terminal detection according to https://github.com/termstandard/colors/ * Add detection for 256 colors terminal (Ansi8 support) and add the conversion from true color (Ansi24) to it. This is especially useful for the Apple Terminal which is still deployed by default in 2022, but not only. Commits ------- 3f90257 [Console] PR #46944 - Adds support for Ansi8 (256 colors) and improves terminal support detection (Ansi4, Ansi8, Ansi24)
2 parents 41ebf12 + 3f90257 commit 6c8c93d

File tree

7 files changed

+323
-58
lines changed

7 files changed

+323
-58
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
6.2
5+
---
6+
7+
* Improve truecolor terminal detection in some cases
8+
* Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it)
9+
410
6.1
511
---
612

src/Symfony/Component/Console/Color.php

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,7 @@ private function parseColor(string $color, bool $background = false): string
117117
}
118118

119119
if ('#' === $color[0]) {
120-
$color = substr($color, 1);
121-
122-
if (3 === \strlen($color)) {
123-
$color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
124-
}
125-
126-
if (6 !== \strlen($color)) {
127-
throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
128-
}
129-
130-
return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color));
120+
return ($background ? '4' : '3').Terminal::getTermColorSupport()->convertFromHexToAnsiColorCode($color);
131121
}
132122

133123
if (isset(self::COLORS[$color])) {
@@ -140,41 +130,4 @@ private function parseColor(string $color, bool $background = false): string
140130

141131
throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
142132
}
143-
144-
private function convertHexColorToAnsi(int $color): string
145-
{
146-
$r = ($color >> 16) & 255;
147-
$g = ($color >> 8) & 255;
148-
$b = $color & 255;
149-
150-
// see https://github.com/termstandard/colors/ for more information about true color support
151-
if ('truecolor' !== getenv('COLORTERM')) {
152-
return (string) $this->degradeHexColorToAnsi($r, $g, $b);
153-
}
154-
155-
return sprintf('8;2;%d;%d;%d', $r, $g, $b);
156-
}
157-
158-
private function degradeHexColorToAnsi(int $r, int $g, int $b): int
159-
{
160-
if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
161-
return 0;
162-
}
163-
164-
return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
165-
}
166-
167-
private function getSaturation(int $r, int $g, int $b): int
168-
{
169-
$r = $r / 255;
170-
$g = $g / 255;
171-
$b = $b / 255;
172-
$v = max($r, $g, $b);
173-
174-
if (0 === $diff = $v - min($r, $g, $b)) {
175-
return 0;
176-
}
177-
178-
return (int) $diff * 100 / $v;
179-
}
180133
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Output;
13+
14+
use Symfony\Component\Asset\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @author Fabien Potencier <[email protected]>
18+
* @author Julien Boudry <[email protected]>
19+
*/
20+
enum AnsiColorMode
21+
{
22+
/**
23+
* Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m"
24+
* Must be compatible with all terminals and it's the minimal version supported.
25+
*/
26+
case Ansi4;
27+
28+
/**
29+
* 8-bit Ansi colors (240 differents colors + 16 duplicate color codes, ensuring backward compatibility).
30+
* Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m"
31+
* Should be compatible with most terminals.
32+
*/
33+
case Ansi8;
34+
35+
/**
36+
* 24-bit Ansi colors (RGB).
37+
* Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m"
38+
* May be compatible with many modern terminals.
39+
*/
40+
case Ansi24;
41+
42+
/**
43+
* Converts an RGB hexadecimal color to the corresponding Ansi code.
44+
*/
45+
public function convertFromHexToAnsiColorCode(string $hexColor): string
46+
{
47+
$hexColor = str_replace('#', '', $hexColor);
48+
49+
if (3 === \strlen($hexColor)) {
50+
$hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2];
51+
}
52+
53+
if (6 !== \strlen($hexColor)) {
54+
throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor));
55+
}
56+
57+
$color = hexdec($hexColor);
58+
59+
$r = ($color >> 16) & 255;
60+
$g = ($color >> 8) & 255;
61+
$b = $color & 255;
62+
63+
return match ($this) {
64+
self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b),
65+
self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)),
66+
self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b)
67+
};
68+
}
69+
70+
private function convertFromRGB(int $r, int $g, int $b): int
71+
{
72+
return match ($this) {
73+
self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b),
74+
self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b),
75+
default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.")
76+
};
77+
}
78+
79+
private function degradeHexColorToAnsi4(int $r, int $g, int $b): int
80+
{
81+
if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
82+
return 0;
83+
}
84+
85+
return (int) ((round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255));
86+
}
87+
88+
private function getSaturation(int $r, int $g, int $b): int
89+
{
90+
$r = $r / 255;
91+
$g = $g / 255;
92+
$b = $b / 255;
93+
$v = max($r, $g, $b);
94+
95+
if (0 === $diff = $v - min($r, $g, $b)) {
96+
return 0;
97+
}
98+
99+
return (int) ((int) $diff * 100 / $v);
100+
}
101+
102+
/**
103+
* Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license).
104+
*/
105+
private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
106+
{
107+
if ($r === $g && $g === $b) {
108+
if ($r < 8) {
109+
return 16;
110+
}
111+
112+
if ($r > 248) {
113+
return 231;
114+
}
115+
116+
return (int) round(($r - 8) / 247 * 24) + 232;
117+
} else {
118+
return 16 +
119+
(36 * (int) round($r / 255 * 5)) +
120+
(6 * (int) round($g / 255 * 5)) +
121+
(int) round($b / 255 * 5);
122+
}
123+
}
124+
}

src/Symfony/Component/Console/Terminal.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,49 @@
1111

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Output\AnsiColorMode;
15+
1416
class Terminal
1517
{
1618
private static ?int $width = null;
1719
private static ?int $height = null;
1820
private static ?bool $stty = null;
1921

22+
/**
23+
* About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
24+
* For more information about true color support with terminals https://github.com/termstandard/colors/.
25+
*/
26+
public static function getTermColorSupport(): AnsiColorMode
27+
{
28+
// Try with $COLORTERM first
29+
if (\is_string($colorterm = getenv('COLORTERM'))) {
30+
$colorterm = strtolower($colorterm);
31+
32+
if (str_contains($colorterm, 'truecolor')) {
33+
return AnsiColorMode::Ansi24;
34+
}
35+
36+
if (str_contains($colorterm, '256color')) {
37+
return AnsiColorMode::Ansi8;
38+
}
39+
}
40+
41+
// Try with $TERM
42+
if (\is_string($term = getenv('TERM'))) {
43+
$term = strtolower($term);
44+
45+
if (str_contains($term, 'truecolor')) {
46+
return AnsiColorMode::Ansi24;
47+
}
48+
49+
if (str_contains($term, '256color')) {
50+
return AnsiColorMode::Ansi8;
51+
}
52+
}
53+
54+
return AnsiColorMode::Ansi4;
55+
}
56+
2057
/**
2158
* Gets the terminal width.
2259
*/

src/Symfony/Component/Console/Tests/ColorTest.php

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
class ColorTest extends TestCase
1818
{
19-
public function testAnsiColors()
19+
public function testAnsi4Colors()
2020
{
2121
$color = new Color();
2222
$this->assertSame(' ', $color->apply(' '));
@@ -33,21 +33,27 @@ public function testAnsiColors()
3333

3434
public function testTrueColors()
3535
{
36-
if ('truecolor' !== getenv('COLORTERM')) {
37-
$this->markTestSkipped('True color not supported.');
38-
}
36+
$colorterm = getenv('COLORTERM');
37+
putenv('COLORTERM=truecolor');
3938

40-
$color = new Color('#fff', '#000');
41-
$this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
39+
try {
40+
$color = new Color('#fff', '#000');
41+
$this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
4242

43-
$color = new Color('#ffffff', '#000000');
44-
$this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
43+
$color = new Color('#ffffff', '#000000');
44+
$this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
45+
} finally {
46+
(false !== $colorterm) ? putenv('COLORTERM='.$colorterm) : putenv('COLORTERM');
47+
}
4548
}
4649

47-
public function testDegradedTrueColors()
50+
public function testDegradedTrueColorsToAnsi4()
4851
{
4952
$colorterm = getenv('COLORTERM');
53+
$term = getenv('TERM');
54+
5055
putenv('COLORTERM=');
56+
putenv('TERM=');
5157

5258
try {
5359
$color = new Color('#f00', '#ff0');
@@ -56,7 +62,28 @@ public function testDegradedTrueColors()
5662
$color = new Color('#c0392b', '#f1c40f');
5763
$this->assertSame("\033[31;43m \033[39;49m", $color->apply(' '));
5864
} finally {
59-
putenv('COLORTERM='.$colorterm);
65+
(false !== $colorterm) ? putenv('COLORTERM='.$colorterm) : putenv('COLORTERM');
66+
(false !== $term) ? putenv('TERM='.$term) : putenv('TERM');
67+
}
68+
}
69+
70+
public function testDegradedTrueColorsToAnsi8()
71+
{
72+
$colorterm = getenv('COLORTERM');
73+
$term = getenv('TERM');
74+
75+
putenv('COLORTERM=');
76+
putenv('TERM=symfonyTest-256color');
77+
78+
try {
79+
$color = new Color('#f57255', '#8993c0');
80+
$this->assertSame("\033[38;5;210;48;5;146m \033[39;49m", $color->apply(' '));
81+
82+
$color = new Color('#000000', '#ffffff');
83+
$this->assertSame("\033[38;5;16;48;5;231m \033[39;49m", $color->apply(' '));
84+
} finally {
85+
(false !== $colorterm) ? putenv('COLORTERM='.$colorterm) : putenv('COLORTERM');
86+
(false !== $term) ? putenv('TERM='.$term) : putenv('TERM');
6087
}
6188
}
6289
}

0 commit comments

Comments
 (0)