Passed
Push — master ( 3a5cce...db8d65 )
by Alec
02:35
created

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