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@2: declare(strict_types=1); Chris@2: Chris@0: namespace SebastianBergmann\Environment; Chris@0: Chris@2: final class Console Chris@0: { Chris@2: /** Chris@2: * @var int Chris@2: */ Chris@0: const STDIN = 0; Chris@2: Chris@2: /** Chris@2: * @var int Chris@2: */ Chris@0: const STDOUT = 1; Chris@2: Chris@2: /** Chris@2: * @var int Chris@2: */ 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@2: public function hasColorSupport(): bool Chris@0: { Chris@2: if ($this->isWindows()) { Chris@2: // @codeCoverageIgnoreStart Chris@2: return false !== \getenv('ANSICON') || 'ON' === \getenv('ConEmuANSI') || 'xterm' === \getenv('TERM'); Chris@2: // @codeCoverageIgnoreEnd Chris@0: } Chris@0: Chris@2: if (!\defined('STDOUT')) { Chris@2: // @codeCoverageIgnoreStart Chris@0: return false; Chris@2: // @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@2: * @codeCoverageIgnore Chris@0: */ Chris@2: public function getNumberOfColumns(): int Chris@0: { Chris@2: if ($this->isWindows()) { Chris@2: return $this->getNumberOfColumnsWindows(); Chris@0: } Chris@0: Chris@0: if (!$this->isInteractive(self::STDIN)) { Chris@0: return 80; Chris@0: } Chris@0: Chris@2: return $this->getNumberOfColumnsInteractive(); Chris@2: } Chris@2: Chris@2: /** Chris@2: * Returns if the file descriptor is an interactive terminal or not. Chris@2: * Chris@2: * @param int|resource $fileDescriptor Chris@2: */ Chris@2: public function isInteractive($fileDescriptor = self::STDOUT): bool Chris@2: { Chris@2: return \function_exists('posix_isatty') && @\posix_isatty($fileDescriptor); Chris@2: } Chris@2: Chris@2: private function isWindows(): bool Chris@2: { Chris@2: return DIRECTORY_SEPARATOR === '\\'; Chris@2: } Chris@2: Chris@2: /** Chris@2: * @codeCoverageIgnore Chris@2: */ Chris@2: private function getNumberOfColumnsInteractive(): int Chris@2: { Chris@2: 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@2: 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@2: * @codeCoverageIgnore Chris@0: */ Chris@2: private function getNumberOfColumnsWindows(): int Chris@0: { Chris@2: $ansicon = \getenv('ANSICON'); Chris@2: $columns = 80; Chris@2: Chris@2: if (\is_string($ansicon) && \preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', \trim($ansicon), $matches)) { Chris@2: $columns = $matches[1]; Chris@2: } elseif (\function_exists('proc_open')) { Chris@2: $process = \proc_open( Chris@2: 'mode CON', Chris@2: [ Chris@2: 1 => ['pipe', 'w'], Chris@2: 2 => ['pipe', 'w'] Chris@2: ], Chris@2: $pipes, Chris@2: null, Chris@2: null, Chris@2: ['suppress_errors' => true] Chris@2: ); Chris@2: Chris@2: if (\is_resource($process)) { Chris@2: $info = \stream_get_contents($pipes[1]); Chris@2: Chris@2: \fclose($pipes[1]); Chris@2: \fclose($pipes[2]); Chris@2: \proc_close($process); Chris@2: Chris@2: if (\preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { Chris@2: $columns = $matches[2]; Chris@2: } Chris@2: } Chris@2: } Chris@2: Chris@2: return $columns - 1; Chris@0: } Chris@0: }