Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@14: declare(strict_types=1); Chris@14: Chris@0: namespace SebastianBergmann\Environment; Chris@0: Chris@14: final class Console Chris@0: { Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@0: const STDIN = 0; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@0: const STDOUT = 1; Chris@14: Chris@14: /** Chris@14: * @var int Chris@14: */ Chris@0: const STDERR = 2; Chris@0: Chris@0: /** Chris@0: * Returns true if STDOUT supports colorization. Chris@0: * Chris@0: * This code has been copied and adapted from Chris@0: * Symfony\Component\Console\Output\OutputStream. Chris@0: */ Chris@14: public function hasColorSupport(): bool Chris@0: { Chris@14: if ($this->isWindows()) { Chris@14: // @codeCoverageIgnoreStart Chris@14: return false !== \getenv('ANSICON') || 'ON' === \getenv('ConEmuANSI') || 'xterm' === \getenv('TERM'); Chris@14: // @codeCoverageIgnoreEnd Chris@0: } Chris@0: Chris@14: if (!\defined('STDOUT')) { Chris@14: // @codeCoverageIgnoreStart Chris@0: return false; Chris@14: // @codeCoverageIgnoreEnd Chris@0: } Chris@0: Chris@0: return $this->isInteractive(STDOUT); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the number of columns of the terminal. Chris@0: * Chris@14: * @codeCoverageIgnore Chris@0: */ Chris@14: public function getNumberOfColumns(): int Chris@0: { Chris@14: if ($this->isWindows()) { Chris@14: return $this->getNumberOfColumnsWindows(); Chris@0: } Chris@0: Chris@0: if (!$this->isInteractive(self::STDIN)) { Chris@0: return 80; Chris@0: } Chris@0: Chris@14: return $this->getNumberOfColumnsInteractive(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns if the file descriptor is an interactive terminal or not. Chris@14: * Chris@14: * @param int|resource $fileDescriptor Chris@14: */ Chris@14: public function isInteractive($fileDescriptor = self::STDOUT): bool Chris@14: { Chris@14: return \function_exists('posix_isatty') && @\posix_isatty($fileDescriptor); Chris@14: } Chris@14: Chris@14: private function isWindows(): bool Chris@14: { Chris@14: return DIRECTORY_SEPARATOR === '\\'; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @codeCoverageIgnore Chris@14: */ Chris@14: private function getNumberOfColumnsInteractive(): int Chris@14: { Chris@14: if (\function_exists('shell_exec') && \preg_match('#\d+ (\d+)#', \shell_exec('stty size') ?? '', $match) === 1) { Chris@0: if ((int) $match[1] > 0) { Chris@0: return (int) $match[1]; Chris@0: } Chris@0: } Chris@0: Chris@14: if (\function_exists('shell_exec') && \preg_match('#columns = (\d+);#', \shell_exec('stty') ?? '', $match) === 1) { Chris@0: if ((int) $match[1] > 0) { Chris@0: return (int) $match[1]; Chris@0: } Chris@0: } Chris@0: Chris@0: return 80; Chris@0: } Chris@0: Chris@0: /** Chris@14: * @codeCoverageIgnore Chris@0: */ Chris@14: private function getNumberOfColumnsWindows(): int Chris@0: { Chris@14: $ansicon = \getenv('ANSICON'); Chris@14: $columns = 80; Chris@14: Chris@14: if (\is_string($ansicon) && \preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', \trim($ansicon), $matches)) { Chris@14: $columns = $matches[1]; Chris@14: } elseif (\function_exists('proc_open')) { Chris@14: $process = \proc_open( Chris@14: 'mode CON', Chris@14: [ Chris@14: 1 => ['pipe', 'w'], Chris@14: 2 => ['pipe', 'w'] Chris@14: ], Chris@14: $pipes, Chris@14: null, Chris@14: null, Chris@14: ['suppress_errors' => true] Chris@14: ); Chris@14: Chris@14: if (\is_resource($process)) { Chris@14: $info = \stream_get_contents($pipes[1]); Chris@14: Chris@14: \fclose($pipes[1]); Chris@14: \fclose($pipes[2]); Chris@14: \proc_close($process); Chris@14: Chris@14: if (\preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { Chris@14: $columns = $matches[2]; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $columns - 1; Chris@0: } Chris@0: }