Skip to content

Commit

Permalink
Merge pull request #3165 from paulbalandan/cli-color-detection
Browse files Browse the repository at this point in the history
More robust color support detection in CLI
  • Loading branch information
lonnieezell authored Jun 25, 2020
2 parents 55f947f + e891813 commit 6b23f2e
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 67 deletions.
76 changes: 71 additions & 5 deletions system/CLI/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -468,10 +468,10 @@ public static function clearScreen()
static::isWindows()

// Windows is a bit crap at this, but their terminal is tiny so shove this in
? static::newLine(40)
? static::newLine(40)

// Anything with a flair of Unix will handle these magic characters
: fwrite(STDOUT, chr(27) . '[H' . chr(27) . '[2J');
: fwrite(STDOUT, chr(27) . '[H' . chr(27) . '[2J');
}

//--------------------------------------------------------------------
Expand All @@ -489,11 +489,9 @@ public static function clearScreen()
*/
public static function color(string $text, string $foreground, string $background = null, string $format = null): string
{
if (static::isWindows() && ! isset($_SERVER['ANSICON']))
if (! static::hasColorSupport(STDOUT))
{
// @codeCoverageIgnoreStart
return $text;
// @codeCoverageIgnoreEnd
}

if (! array_key_exists($foreground, static::$foreground_colors))
Expand Down Expand Up @@ -537,6 +535,7 @@ public static function strlen(?string $string): int
{
return 0;
}

foreach (static::$foreground_colors as $color)
{
$string = strtr($string, ["\033[" . $color . 'm' => '']);
Expand All @@ -554,6 +553,73 @@ public static function strlen(?string $string): int

//--------------------------------------------------------------------

/**
* Checks whether the current stream resource supports or
* refers to a valid terminal type device.
*
* @param string $function
* @param resource $resource
*
* @return boolean
*/
public static function streamSupports(string $function, $resource): bool
{
if (ENVIRONMENT === 'testing')
{
// In the current setup of the tests we cannot fully check
// if the stream supports the function since we are using
// filtered streams.
return function_exists($function);
}

// @codeCoverageIgnoreStart
return function_exists($function) && @$function($resource);
// @codeCoverageIgnoreEnd
}

//--------------------------------------------------------------------

/**
* Returns true if the stream resource supports colors.
*
* This is tricky on Windows, because Cygwin, Msys2 etc. emulate pseudo
* terminals via named pipes, so we can only check the environment.
*
* Reference: https://github.com/composer/xdebug-handler/blob/master/src/Process.php
*
* @param resource $resource
*
* @return boolean
*/
public static function hasColorSupport($resource): bool
{
// Follow https://no-color.org/
if (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR') !== false)
{
return false;
}

if (getenv('TERM_PROGRAM') === 'Hyper')
{
return true;
}

if (static::isWindows())
{
// @codeCoverageIgnoreStart
return static::streamSupports('sapi_windows_vt100_support', $resource)
|| isset($_SERVER['ANSICON'])
|| getenv('ANSICON') !== false
|| getenv('ConEmuANSI') === 'ON'
|| getenv('TERM') === 'xterm';
// @codeCoverageIgnoreEnd
}

return static::streamSupports('stream_isatty', $resource);
}

//--------------------------------------------------------------------

/**
* Attempts to determine the width of the viewable CLI window.
*
Expand Down
123 changes: 61 additions & 62 deletions tests/system/CLI/CLITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ public function testColorExceptionBackground()
CLI::color('test', 'white', 'Background');
}

public function testColorSupportOnNoColor()
{
$nocolor = getenv('NO_COLOR');
putenv('NO_COLOR=1');

$this->assertEquals('test', CLI::color('test', 'white', 'green'));
putenv($nocolor ? "NO_COLOR=$nocolor" : 'NO_COLOR');
}

public function testColorSupportOnHyperTerminals()
{
$termProgram = getenv('TERM_PROGRAM');
putenv('TERM_PROGRAM=Hyper');

$this->assertEquals("\033[1;37m\033[42m\033[4mtest\033[0m", CLI::color('test', 'white', 'green', 'underline'));
putenv($termProgram ? "TERM_PROGRAM=$termProgram" : 'TERM_PROGRAM');
}

public function testStreamSupports()
{
$this->assertTrue(CLI::streamSupports('stream_isatty', STDOUT));
$this->assertIsBool(CLI::streamSupports('sapi_windows_vt100_support', STDOUT));
}

public function testColor()
{
$this->assertEquals("\033[1;37m\033[42m\033[4mtest\033[0m", CLI::color('test', 'white', 'green', 'underline'));
Expand Down Expand Up @@ -124,31 +148,21 @@ public function testPrintBackground()
public function testWrite()
{
CLI::write('test');
$expected = <<<EOT
test
EOT;
$expected = PHP_EOL . 'test' . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

public function testWriteForeground()
{
CLI::write('test', 'red');
$expected = <<<EOT
\033[0;31mtest\033[0m
EOT;
$expected = "\033[0;31mtest\033[0m" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

public function testWriteBackground()
{
CLI::write('test', 'red', 'green');
$expected = <<<EOT
\033[0;31m\033[42mtest\033[0m
EOT;
$expected = "\033[0;31m\033[42mtest\033[0m" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

Expand All @@ -157,32 +171,23 @@ public function testError()
$this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter');
CLI::error('test');
// red expected cuz stderr
$expected = <<<EOT
\033[1;31mtest\033[0m
EOT;
$expected = "\033[1;31mtest\033[0m" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

public function testErrorForeground()
{
$this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter');
CLI::error('test', 'purple');
$expected = <<<EOT
\033[0;35mtest\033[0m
EOT;
$expected = "\033[0;35mtest\033[0m" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

public function testErrorBackground()
{
$this->stream_filter = stream_filter_append(STDERR, 'CITestStreamFilter');
CLI::error('test', 'purple', 'green');
$expected = <<<EOT
\033[0;35m\033[42mtest\033[0m
EOT;
$expected = "\033[0;35m\033[42mtest\033[0m" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

Expand All @@ -199,19 +204,16 @@ public function testShowProgress()
CLI::write('third.');
CLI::showProgress(1, 20);

$expected = <<<EOT
first.
[\033[32m#.........\033[0m] 5% Complete
\033[1A[\033[32m#####.....\033[0m] 50% Complete
\033[1A[\033[32m##########\033[0m] 100% Complete
second.
[\033[32m#.........\033[0m] 5% Complete
\033[1A[\033[32m#####.....\033[0m] 50% Complete
\033[1A[\033[32m##########\033[0m] 100% Complete
third.
[\033[32m#.........\033[0m] 5% Complete
EOT;
$expected = 'first.' . PHP_EOL .
"[\033[32m#.........\033[0m] 5% Complete" . PHP_EOL .
"\033[1A[\033[32m#####.....\033[0m] 50% Complete" . PHP_EOL .
"\033[1A[\033[32m##########\033[0m] 100% Complete" . PHP_EOL .
'second.' . PHP_EOL .
"[\033[32m#.........\033[0m] 5% Complete" . PHP_EOL .
"\033[1A[\033[32m#####.....\033[0m] 50% Complete" . PHP_EOL .
"\033[1A[\033[32m##########\033[0m] 100% Complete" . PHP_EOL .
'third.' . PHP_EOL .
"[\033[32m#.........\033[0m] 5% Complete" . PHP_EOL;
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

Expand All @@ -222,10 +224,7 @@ public function testShowProgressWithoutBar()
CLI::showProgress(false, 20);
CLI::showProgress(false, 20);

$expected = <<<EOT
first.
\007\007\007
EOT;
$expected = 'first.' . PHP_EOL . "\007\007\007";
$this->assertEquals($expected, CITestStreamFilter::$buffer);
}

Expand Down Expand Up @@ -365,38 +364,38 @@ public function tableProvider()
[
$one_row,
[],
"+---+-----+\n" .
"| 1 | bar |\n" .
"+---+-----+\n\n",
'+---+-----+' . PHP_EOL .
'| 1 | bar |' . PHP_EOL .
'+---+-----+' . PHP_EOL . PHP_EOL,
],
[
$one_row,
$head,
"+----+-------+\n" .
"| ID | Title |\n" .
"+----+-------+\n" .
"| 1 | bar |\n" .
"+----+-------+\n\n",
'+----+-------+' . PHP_EOL .
'| ID | Title |' . PHP_EOL .
'+----+-------+' . PHP_EOL .
'| 1 | bar |' . PHP_EOL .
'+----+-------+' . PHP_EOL . PHP_EOL,
],
[
$many_rows,
[],
"+---+-----------------+\n" .
"| 1 | bar |\n" .
"| 2 | bar * 2 |\n" .
"| 3 | bar + bar + bar |\n" .
"+---+-----------------+\n\n",
'+---+-----------------+' . PHP_EOL .
'| 1 | bar |' . PHP_EOL .
'| 2 | bar * 2 |' . PHP_EOL .
'| 3 | bar + bar + bar |' . PHP_EOL .
'+---+-----------------+' . PHP_EOL . PHP_EOL,
],
[
$many_rows,
$head,
"+----+-----------------+\n" .
"| ID | Title |\n" .
"+----+-----------------+\n" .
"| 1 | bar |\n" .
"| 2 | bar * 2 |\n" .
"| 3 | bar + bar + bar |\n" .
"+----+-----------------+\n\n",
'+----+-----------------+' . PHP_EOL .
'| ID | Title |' . PHP_EOL .
'+----+-----------------+' . PHP_EOL .
'| 1 | bar |' . PHP_EOL .
'| 2 | bar * 2 |' . PHP_EOL .
'| 3 | bar + bar + bar |' . PHP_EOL .
'+----+-----------------+' . PHP_EOL . PHP_EOL,
],
];
}
Expand Down

0 comments on commit 6b23f2e

Please sign in to comment.