Passed
Push — master ( 4442ca...f9c178 )
by Alec
02:25
created

Terminal::check256ColorSupport()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

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