Console.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of sebastian/environment.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\Environment;
  11. final class Console
  12. {
  13. /**
  14. * @var int
  15. */
  16. public const STDIN = 0;
  17. /**
  18. * @var int
  19. */
  20. public const STDOUT = 1;
  21. /**
  22. * @var int
  23. */
  24. public const STDERR = 2;
  25. /**
  26. * Returns true if STDOUT supports colorization.
  27. *
  28. * This code has been copied and adapted from
  29. * Symfony\Component\Console\Output\StreamOutput.
  30. */
  31. public function hasColorSupport(): bool
  32. {
  33. if ('Hyper' === \getenv('TERM_PROGRAM')) {
  34. return true;
  35. }
  36. if ($this->isWindows()) {
  37. // @codeCoverageIgnoreStart
  38. return (\defined('STDOUT') && \function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support(\STDOUT))
  39. || false !== \getenv('ANSICON')
  40. || 'ON' === \getenv('ConEmuANSI')
  41. || 'xterm' === \getenv('TERM');
  42. // @codeCoverageIgnoreEnd
  43. }
  44. if (!\defined('STDOUT')) {
  45. // @codeCoverageIgnoreStart
  46. return false;
  47. // @codeCoverageIgnoreEnd
  48. }
  49. return $this->isInteractive(\STDOUT);
  50. }
  51. /**
  52. * Returns the number of columns of the terminal.
  53. *
  54. * @codeCoverageIgnore
  55. */
  56. public function getNumberOfColumns(): int
  57. {
  58. if (!$this->isInteractive(\defined('STDIN') ? \STDIN : self::STDIN)) {
  59. return 80;
  60. }
  61. if ($this->isWindows()) {
  62. return $this->getNumberOfColumnsWindows();
  63. }
  64. return $this->getNumberOfColumnsInteractive();
  65. }
  66. /**
  67. * Returns if the file descriptor is an interactive terminal or not.
  68. *
  69. * Normally, we want to use a resource as a parameter, yet sadly it's not always awailable,
  70. * eg when running code in interactive console (`php -a`), STDIN/STDOUT/STDERR constants are not defined.
  71. *
  72. * @param int|resource $fileDescriptor
  73. */
  74. public function isInteractive($fileDescriptor = self::STDOUT): bool
  75. {
  76. if (\is_resource($fileDescriptor)) {
  77. // These functions require a descriptor that is a real resource, not a numeric ID of it
  78. if (\function_exists('stream_isatty') && @\stream_isatty($fileDescriptor)) {
  79. return true;
  80. }
  81. $stat = @\fstat(\STDOUT);
  82. // Check if formatted mode is S_IFCHR
  83. return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
  84. }
  85. return \function_exists('posix_isatty') && @\posix_isatty($fileDescriptor);
  86. }
  87. private function isWindows(): bool
  88. {
  89. return \DIRECTORY_SEPARATOR === '\\';
  90. }
  91. /**
  92. * @codeCoverageIgnore
  93. */
  94. private function getNumberOfColumnsInteractive(): int
  95. {
  96. if (\function_exists('shell_exec') && \preg_match('#\d+ (\d+)#', \shell_exec('stty size') ?: '', $match) === 1) {
  97. if ((int) $match[1] > 0) {
  98. return (int) $match[1];
  99. }
  100. }
  101. if (\function_exists('shell_exec') && \preg_match('#columns = (\d+);#', \shell_exec('stty') ?: '', $match) === 1) {
  102. if ((int) $match[1] > 0) {
  103. return (int) $match[1];
  104. }
  105. }
  106. return 80;
  107. }
  108. /**
  109. * @codeCoverageIgnore
  110. */
  111. private function getNumberOfColumnsWindows(): int
  112. {
  113. $ansicon = \getenv('ANSICON');
  114. $columns = 80;
  115. if (\is_string($ansicon) && \preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', \trim($ansicon), $matches)) {
  116. $columns = $matches[1];
  117. } elseif (\function_exists('proc_open')) {
  118. $process = \proc_open(
  119. 'mode CON',
  120. [
  121. 1 => ['pipe', 'w'],
  122. 2 => ['pipe', 'w'],
  123. ],
  124. $pipes,
  125. null,
  126. null,
  127. ['suppress_errors' => true]
  128. );
  129. if (\is_resource($process)) {
  130. $info = \stream_get_contents($pipes[1]);
  131. \fclose($pipes[1]);
  132. \fclose($pipes[2]);
  133. \proc_close($process);
  134. if (\preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
  135. $columns = $matches[2];
  136. }
  137. }
  138. }
  139. return $columns - 1;
  140. }
  141. }