1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Console; 13 14class Terminal 15{ 16 private static $width; 17 private static $height; 18 private static $stty; 19 20 /** 21 * Gets the terminal width. 22 * 23 * @return int 24 */ 25 public function getWidth() 26 { 27 $width = getenv('COLUMNS'); 28 if (false !== $width) { 29 return (int) trim($width); 30 } 31 32 if (null === self::$width) { 33 self::initDimensions(); 34 } 35 36 return self::$width ?: 80; 37 } 38 39 /** 40 * Gets the terminal height. 41 * 42 * @return int 43 */ 44 public function getHeight() 45 { 46 $height = getenv('LINES'); 47 if (false !== $height) { 48 return (int) trim($height); 49 } 50 51 if (null === self::$height) { 52 self::initDimensions(); 53 } 54 55 return self::$height ?: 50; 56 } 57 58 /** 59 * @internal 60 * 61 * @return bool 62 */ 63 public static function hasSttyAvailable() 64 { 65 if (null !== self::$stty) { 66 return self::$stty; 67 } 68 69 // skip check if exec function is disabled 70 if (!\function_exists('exec')) { 71 return false; 72 } 73 74 exec('stty 2>&1', $output, $exitcode); 75 76 return self::$stty = 0 === $exitcode; 77 } 78 79 private static function initDimensions() 80 { 81 if ('\\' === \DIRECTORY_SEPARATOR) { 82 if (preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim(getenv('ANSICON')), $matches)) { 83 // extract [w, H] from "wxh (WxH)" 84 // or [w, h] from "wxh" 85 self::$width = (int) $matches[1]; 86 self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; 87 } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) { 88 // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) 89 // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT 90 self::initDimensionsUsingStty(); 91 } elseif (null !== $dimensions = self::getConsoleMode()) { 92 // extract [w, h] from "wxh" 93 self::$width = (int) $dimensions[0]; 94 self::$height = (int) $dimensions[1]; 95 } 96 } else { 97 self::initDimensionsUsingStty(); 98 } 99 } 100 101 /** 102 * Returns whether STDOUT has vt100 support (some Windows 10+ configurations). 103 */ 104 private static function hasVt100Support(): bool 105 { 106 return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w')); 107 } 108 109 /** 110 * Initializes dimensions using the output of an stty columns line. 111 */ 112 private static function initDimensionsUsingStty() 113 { 114 if ($sttyString = self::getSttyColumns()) { 115 if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { 116 // extract [w, h] from "rows h; columns w;" 117 self::$width = (int) $matches[2]; 118 self::$height = (int) $matches[1]; 119 } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { 120 // extract [w, h] from "; h rows; w columns" 121 self::$width = (int) $matches[2]; 122 self::$height = (int) $matches[1]; 123 } 124 } 125 } 126 127 /** 128 * Runs and parses mode CON if it's available, suppressing any error output. 129 * 130 * @return int[]|null An array composed of the width and the height or null if it could not be parsed 131 */ 132 private static function getConsoleMode(): ?array 133 { 134 $info = self::readFromProcess('mode CON'); 135 136 if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { 137 return null; 138 } 139 140 return [(int) $matches[2], (int) $matches[1]]; 141 } 142 143 /** 144 * Runs and parses stty -a if it's available, suppressing any error output. 145 */ 146 private static function getSttyColumns(): ?string 147 { 148 return self::readFromProcess('stty -a | grep columns'); 149 } 150 151 private static function readFromProcess(string $command): ?string 152 { 153 if (!\function_exists('proc_open')) { 154 return null; 155 } 156 157 $descriptorspec = [ 158 1 => ['pipe', 'w'], 159 2 => ['pipe', 'w'], 160 ]; 161 162 $process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); 163 if (!\is_resource($process)) { 164 return null; 165 } 166 167 $info = stream_get_contents($pipes[1]); 168 fclose($pipes[1]); 169 fclose($pipes[2]); 170 proc_close($process); 171 172 return $info; 173 } 174} 175