Passed
Push — master ( 245dd1...4442ca )
by Alec
02:26
created

Terminal::getSttyColumns()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 19
nc 3
nop 0
dl 0
loc 29
ccs 0
cts 0
cp 0
crap 20
rs 9.6333
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
 * @author Fabien Potencier <[email protected]>
8
 */
9
10
namespace Symfony;
11
12
/**
13
 * Class Terminal
14
 * @author AlecRabbit
15
 */
16
class Terminal
17
{
18
    public const DEFAULT_WIDTH = 80;
19
    public const DEFAULT_HEIGHT = 50;
20
21
    /** @var null|int */
22
    private static $width;
23
24
    /** @var null|int */
25
    private static $height;
26
27
    /** @var null|bool */
28
    private static $supports256Color;
29
30
    /** @var null|bool */
31
    private static $supportsColor;
32
33
    /**
34
     * @param bool $recheck
35
     * @return int
36
     */
37 2
    public function width(bool $recheck = false): int
38
    {
39 2
        if (null !== static::$width && true !== $recheck) {
0 ignored issues
show
Bug introduced by
Since $width is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $width to at least protected.
Loading history...
40 2
            return static::$width;
41
        }
42
        return
43 2
            static::$width = $this->getWidth();
44
    }
45
46
    /**
47
     * Gets the terminal width.
48
     *
49
     * @return int
50
     */
51 2
    protected function getWidth(): int
52
    {
53 2
        $width = \getenv('COLUMNS');
54 2
        if (false !== $width) {
55 2
            return (int)\trim($width);
56
        }
57
        // @codeCoverageIgnoreStart
58
        if (null === self::$width) {
59
            self::initDimensions();
60
        }
61
        return self::$width ?: static::DEFAULT_WIDTH;
62
        // @codeCoverageIgnoreEnd
63
    }
64
65
    /**
66
     * @codeCoverageIgnore
67
     */
68
    private static function initDimensions(): void
69
    {
70
        if (static::onWindows()) {
71
            if ((false !== $term = \getenv('ANSICON')) &&
72
                \preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', \trim($term), $matches)) {
73
                // extract [w, H] from "wxh (WxH)"
74
                // or [w, h] from "wxh"
75
                self::$width = (int)$matches[1];
76
                self::$height = isset($matches[4]) ? (int)$matches[4] : (int)$matches[2];
77
            } elseif (null !== $dimensions = self::getConsoleMode()) {
78
                // extract [w, h] from "wxh"
79
                self::$width = (int)$dimensions[0];
80
                self::$height = (int)$dimensions[1];
81
            }
82
        } elseif ($sttyString = self::getSttyColumns()) {
83
            if (\preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
84
                // extract [w, h] from "rows h; columns w;"
85
                self::$width = (int)$matches[2];
86
                self::$height = (int)$matches[1];
87
            } elseif (\preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
88
                // extract [w, h] from "; h rows; w columns"
89
                self::$width = (int)$matches[2];
90
                self::$height = (int)$matches[1];
91
            }
92
        }
93
    }
94
95
    /**
96
     * @return bool
97
     */
98 1
    protected static function onWindows(): bool
99
    {
100 1
        return '\\' === \DIRECTORY_SEPARATOR;
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
    private 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
    private 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 bool $recheck
175
     * @return int
176
     */
177 2
    public function height(bool $recheck = false): int
178
    {
179 2
        if (null !== static::$height && true !== $recheck) {
0 ignored issues
show
Bug introduced by
Since $height is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $height to at least protected.
Loading history...
180 2
            return static::$height;
181
        }
182
        return
183 2
            static::$height = $this->getHeight();
184
    }
185
186
    /**
187
     * Gets the terminal height.
188
     *
189
     * @return int
190
     */
191 2
    protected function getHeight(): int
192
    {
193 2
        $height = \getenv('LINES');
194 2
        if (false !== $height) {
195 2
            return (int)\trim($height);
196
        }
197
        // @codeCoverageIgnoreStart
198
        if (null === self::$height) {
199
            self::initDimensions();
200
        }
201
        return self::$height ?: static::DEFAULT_HEIGHT;
202
        // @codeCoverageIgnoreEnd
203
    }
204
205
    /**
206
     * @return bool
207
     */
208 72
    public function supports256Color(): bool
209
    {
210 72
        if (null !== static::$supports256Color) {
0 ignored issues
show
Bug introduced by
Since $supports256Color is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $supports256Color to at least protected.
Loading history...
211 71
            return static::$supports256Color;
212
        }
213
        return
214 1
            static::$supports256Color = $this->check256ColorSupport();
215
    }
216
217
    /**
218
     * @return bool
219
     */
220 1
    protected function check256ColorSupport(): bool
221
    {
222 1
        if (!$this->supportsColor()) {
223
            return false;
224
        }
225 1
        if (!$term = \getenv('TERM')) {
226
            // @codeCoverageIgnoreStart
227
            return false;
228
            // @codeCoverageIgnoreEnd
229
        }
230 1
        return \strpos($term, '256color') !== false;
231
    }
232
233
    /**
234
     * @return bool
235
     */
236 72
    public function supportsColor(): bool
237
    {
238 72
        if (null !== static::$supportsColor) {
0 ignored issues
show
Bug introduced by
Since $supportsColor is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $supportsColor to at least protected.
Loading history...
239 72
            return static::$supportsColor;
240
        }
241
        return
242 1
            static::$supportsColor = $this->hasColorSupport();
243
    }
244
245
    /**
246
     * Returns true if the stream supports colorization.
247
     *
248
     * Colorization is disabled if not supported by the stream:
249
     *
250
     * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo
251
     * terminals via named pipes, so we can only check the environment.
252
     *
253
     * Reference: Composer\XdebugHandler\Process::supportsColor
254
     * https://github.com/composer/xdebug-handler
255
     *
256
     * @return bool true if the stream supports colorization, false otherwise
257
     *
258
     * based on Symfony\Component\Console\Output\StreamOutput::hasColorSupport()
259
     */
260 1
    protected function hasColorSupport(): bool
261
    {
262 1
        if ('Hyper' === \getenv('TERM_PROGRAM')) {
263
            return true;
264
        }
265
266 1
        if (static::onWindows()) {
267
            return (\function_exists('sapi_windows_vt100_support')
268
                    && @\sapi_windows_vt100_support(STDOUT))
269
                || false !== \getenv('ANSICON')
270
                || 'ON' === \getenv('ConEmuANSI')
271
                || 'xterm' === \getenv('TERM');
272
        }
273
274 1
        if (\function_exists('stream_isatty')) {
275 1
            return @\stream_isatty(STDOUT);
276
        }
277
278
        if (\function_exists('posix_isatty')) {
279
            /** @noinspection PhpComposerExtensionStubsInspection */
280
            return @\posix_isatty(STDOUT);
281
        }
282
283
        $stat = @\fstat(STDOUT);
284
        // Check if formatted mode is S_IFCHR
285
        return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
286
    }
287
}
288