Passed
Push — master ( 15e4f7...543a09 )
by Alec
03:07
created

Terminal::getHeight()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 0
dl 0
loc 11
ccs 4
cts 4
cp 1
crap 4
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/*
3
 * This class based on
4
 * `Symfony\Component\Console\Terminal::class`
5
 * from `symfony\console` package.
6
 *
7
 * hasColorSupport() based on function
8
 * `Symfony\Component\Console\Output\StreamOutput::hasColorSupport()`
9
 *
10
 * @author Fabien Potencier <[email protected]>
11
 */
12
13
namespace AlecRabbit\ConsoleColour;
14
15
/**
16
 * Class Terminal
17
 * @author AlecRabbit
18
 */
19
class Terminal
20
{
21
    public const DEFAULT_WIDTH = 80;
22
    public const DEFAULT_HEIGHT = 50;
23
24
    /** @var null|int */
25
    protected static $width;
26
27
    /** @var null|int */
28
    protected static $height;
29
30
    /** @var null|bool */
31
    protected static $supports256Color;
32
33
    /** @var null|bool */
34
    protected static $supportsColor;
35
36
    /**
37
     * @param bool $recheck
38
     * @return int
39
     */
40 2
    public function width(bool $recheck = false): int
41
    {
42 2
        if (null !== static::$width && true !== $recheck) {
43 2
            return static::$width;
44
        }
45
        return
46 2
            static::$width = $this->getWidth();
47
    }
48
49
    /**
50
     * Gets the terminal width.
51
     *
52
     * @return int
53
     */
54 2
    protected function getWidth(): int
55
    {
56 2
        $width = \getenv('COLUMNS');
57 2
        if (false !== $width) {
58 2
            return (int)\trim($width);
59
        }
60
        // @codeCoverageIgnoreStart
61
        if (null === static::$width) {
62
            static::initDimensions();
63
        }
64
        return static::$width ?: static::DEFAULT_WIDTH;
65
        // @codeCoverageIgnoreEnd
66
    }
67
68
    /**
69
     * @codeCoverageIgnore
70
     */
71
    protected static function initDimensions(): void
72
    {
73
        if (static::onWindows()) {
74
            self::initDimensionsWindows();
75
        } elseif ($sttyString = static::getSttyColumns()) {
76
            self::initDimensionsUnix($sttyString);
77
        }
78
    }
79
80
    /**
81
     * @return bool
82
     */
83 1
    protected static function onWindows(): bool
84
    {
85 1
        return '\\' === \DIRECTORY_SEPARATOR;
86
    }
87
88
    protected static function initDimensionsWindows(): void
89
    {
90
        if ((false !== $term = \getenv('ANSICON')) &&
91
            \preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', \trim($term), $matches)) {
92
            // extract [w, H] from "wxh (WxH)"
93
            // or [w, h] from "wxh"
94
            static::$width = (int)$matches[1];
95
            static::$height = isset($matches[4]) ? (int)$matches[4] : (int)$matches[2];
96
        } elseif (null !== $dimensions = static::getConsoleMode()) {
97
            // extract [w, h] from "wxh"
98
            static::$width = (int)$dimensions[0];
99
            static::$height = (int)$dimensions[1];
100
        }
101
    }
102
103
    /**
104
     * @codeCoverageIgnore
105
     *
106
     * Runs and parses mode CON if it's available, suppressing any error output.
107
     *
108
     * @return int[]|null An array composed of the width and the height or null if it could not be parsed
109
     */
110
    protected static function getConsoleMode(): ?array
111
    {
112
        if (!\function_exists('proc_open')) {
113
            return null;
114
        }
115
116
        $descriptorSpec = [
117
            1 => ['pipe', 'w'],
118
            2 => ['pipe', 'w'],
119
        ];
120
        $process =
121
            \proc_open('mode CON', $descriptorSpec, $pipes, null, null, ['suppress_errors' => true]);
122
        if (\is_resource($process)) {
123
            $info = \stream_get_contents($pipes[1]);
124
            \fclose($pipes[1]);
125
            \fclose($pipes[2]);
126
            \proc_close($process);
127
128
            if (false !== $info && \preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
129
                return [(int)$matches[2], (int)$matches[1]];
130
            }
131
        }
132
        return null;
133
    }
134
135
    /**
136
     * @codeCoverageIgnore
137
     *
138
     * Runs and parses stty -a if it's available, suppressing any error output.
139
     *
140
     * @return string|null
141
     */
142
    protected static function getSttyColumns(): ?string
143
    {
144
        if (!\function_exists('proc_open')) {
145
            return null;
146
        }
147
148
        $descriptorSpec = [
149
            1 => ['pipe', 'w'],
150
            2 => ['pipe', 'w'],
151
        ];
152
153
        $process = \proc_open(
154
            'stty -a | grep columns',
155
            $descriptorSpec,
156
            $pipes,
157
            null,
158
            null,
159
            ['suppress_errors' => true]
160
        );
161
162
        if (\is_resource($process)) {
163
            $info = \stream_get_contents($pipes[1]);
164
            \fclose($pipes[1]);
165
            \fclose($pipes[2]);
166
            \proc_close($process);
167
168
            return $info ?: null;
169
        }
170
        return null;
171
    }
172
173
    /**
174
     * @param string $sttyString
175
     */
176
    protected static function initDimensionsUnix(string $sttyString): void
177
    {
178
        if (\preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
179
            // extract [w, h] from "rows h; columns w;"
180
            static::$width = (int)$matches[2];
181
            static::$height = (int)$matches[1];
182
        } elseif (\preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
183
            // extract [w, h] from "; h rows; w columns"
184
            static::$width = (int)$matches[2];
185
            static::$height = (int)$matches[1];
186
        }
187
    }
188
189
    /**
190
     * @param bool $recheck
191
     * @return int
192
     */
193 2
    public function height(bool $recheck = false): int
194
    {
195 2
        if (null !== static::$height && true !== $recheck) {
196 2
            return static::$height;
197
        }
198
        return
199 2
            static::$height = $this->getHeight();
200
    }
201
202
    /**
203
     * Gets the terminal height.
204
     *
205
     * @return int
206
     */
207 2
    protected function getHeight(): int
208
    {
209 2
        $height = \getenv('LINES');
210 2
        if (false !== $height) {
211 2
            return (int)\trim($height);
212
        }
213
        // @codeCoverageIgnoreStart
214
        if (null === static::$height) {
215
            static::initDimensions();
216
        }
217
        return static::$height ?: static::DEFAULT_HEIGHT;
218
        // @codeCoverageIgnoreEnd
219
    }
220
221
    /**
222
     * @return bool
223
     */
224 72
    public function supports256Color(): bool
225
    {
226 72
        if (null !== static::$supports256Color) {
227 71
            return static::$supports256Color;
228
        }
229
        return
230 1
            static::$supports256Color = $this->check256ColorSupport();
231
    }
232
233
    /**
234
     * @return bool
235
     */
236 1
    protected function check256ColorSupport(): bool
237
    {
238 1
        if (!$this->supportsColor()) {
239
            return false;
240
        }
241 1
        if (!$term = \getenv('TERM')) {
242
            // @codeCoverageIgnoreStart
243
            return false;
244
            // @codeCoverageIgnoreEnd
245
        }
246 1
        return \strpos($term, '256color') !== false;
247
    }
248
249
    /**
250
     * @return bool
251
     */
252 72
    public function supportsColor(): bool
253
    {
254 72
        if (null !== static::$supportsColor) {
255 72
            return static::$supportsColor;
256
        }
257
        return
258 1
            static::$supportsColor = $this->hasColorSupport();
259
    }
260
261
    /**
262
     * Returns true if the stream supports colorization.
263
     *
264
     * Colorization is disabled if not supported by the stream:
265
     *
266
     * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
267
     * terminals via named pipes, so we can only check the environment.
268
     *
269
     * Reference: Composer\XdebugHandler\Process::supportsColor
270
     * https://github.com/composer/xdebug-handler
271
     *
272
     * @return bool true if the stream supports colorization, false otherwise
273
     */
274 1
    protected function hasColorSupport(): bool
275
    {
276 1
        if ('Hyper' === \getenv('TERM_PROGRAM')) {
277
            return true;
278
        }
279
280 1
        if (static::onWindows()) {
281
            return (\function_exists('sapi_windows_vt100_support')
282
                    && @\sapi_windows_vt100_support(STDOUT))
283
                || false !== \getenv('ANSICON')
284
                || 'ON' === \getenv('ConEmuANSI')
285
                || 'xterm' === \getenv('TERM');
286
        }
287
288 1
        if (\function_exists('stream_isatty')) {
289 1
            return @\stream_isatty(STDOUT);
290
        }
291
292
        if (\function_exists('posix_isatty')) {
293
            /** @noinspection PhpComposerExtensionStubsInspection */
294
            return @\posix_isatty(STDOUT);
295
        }
296
297
        $stat = @\fstat(STDOUT);
298
        // Check if formatted mode is S_IFCHR
299
        return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
300
    }
301
}
302