Passed
Push — master ( db6c3d...cb42e6 )
by Alec
02:29
created

Terminal::check256ColorSupport()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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