Passed
Push — master ( b3847f...c7da00 )
by Alec
02:36
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
     * Gets the terminal width.
35
     *
36
     * @return int
37
     */
38 2
    public function getWidth(): int
39
    {
40 2
        $width = \getenv('COLUMNS');
41 2
        if (false !== $width) {
42 2
            return (int)\trim($width);
43
        }
44
        // @codeCoverageIgnoreStart
45
        if (null === self::$width) {
46
            self::initDimensions();
47
        }
48
        return self::$width ?: static::DEFAULT_WIDTH;
49
        // @codeCoverageIgnoreEnd
50
    }
51
52
    /**
53
     * @codeCoverageIgnore
54
     */
55
    private static function initDimensions(): void
56
    {
57
        if ('\\' === \DIRECTORY_SEPARATOR) {
58
            if ((false !== $term = \getenv('ANSICON')) &&
59
                \preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', \trim($term), $matches)) {
60
                // extract [w, H] from "wxh (WxH)"
61
                // or [w, h] from "wxh"
62
                self::$width = (int)$matches[1];
63
                self::$height = isset($matches[4]) ? (int)$matches[4] : (int)$matches[2];
64
            } elseif (null !== $dimensions = self::getConsoleMode()) {
65
                // extract [w, h] from "wxh"
66
                self::$width = (int)$dimensions[0];
67
                self::$height = (int)$dimensions[1];
68
            }
69
        } elseif ($sttyString = self::getSttyColumns()) {
70
            if (\preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
71
                // extract [w, h] from "rows h; columns w;"
72
                self::$width = (int)$matches[2];
73
                self::$height = (int)$matches[1];
74
            } elseif (\preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
75
                // extract [w, h] from "; h rows; w columns"
76
                self::$width = (int)$matches[2];
77
                self::$height = (int)$matches[1];
78
            }
79
        }
80
    }
81
82
    /**
83
     * @codeCoverageIgnore
84
     *
85
     * Runs and parses mode CON if it's available, suppressing any error output.
86
     *
87
     * @return int[]|null An array composed of the width and the height or null if it could not be parsed
88
     */
89
    private static function getConsoleMode(): ?array
90
    {
91
        if (!\function_exists('proc_open')) {
92
            return null;
93
        }
94
95
        $descriptorSpec = [
96
            1 => ['pipe', 'w'],
97
            2 => ['pipe', 'w'],
98
        ];
99
        $process =
100
            \proc_open('mode CON', $descriptorSpec, $pipes, null, null, ['suppress_errors' => true]);
101
        if (\is_resource($process)) {
102
            $info = \stream_get_contents($pipes[1]);
103
            \fclose($pipes[1]);
104
            \fclose($pipes[2]);
105
            \proc_close($process);
106
107
            if (false !== $info && \preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
108
                return [(int)$matches[2], (int)$matches[1]];
109
            }
110
        }
111
        return null;
112
    }
113
114
    /**
115
     * @codeCoverageIgnore
116
     *
117
     * Runs and parses stty -a if it's available, suppressing any error output.
118
     *
119
     * @return string|null
120
     */
121
    private static function getSttyColumns(): ?string
122
    {
123
        if (!\function_exists('proc_open')) {
124
            return null;
125
        }
126
127
        $descriptorSpec = [
128
            1 => ['pipe', 'w'],
129
            2 => ['pipe', 'w'],
130
        ];
131
132
        $process = \proc_open(
133
            'stty -a | grep columns',
134
            $descriptorSpec,
135
            $pipes,
136
            null,
137
            null,
138
            ['suppress_errors' => true]
139
        );
140
141
        if (\is_resource($process)) {
142
            $info = \stream_get_contents($pipes[1]);
143
            \fclose($pipes[1]);
144
            \fclose($pipes[2]);
145
            \proc_close($process);
146
147
            return $info ?: null;
148
        }
149
        return null;
150
    }
151
152
    /**
153
     * Gets the terminal height.
154
     *
155
     * @return int
156
     */
157 2
    public function getHeight(): int
158
    {
159 2
        $height = \getenv('LINES');
160 2
        if (false !== $height) {
161 2
            return (int)\trim($height);
162
        }
163
        // @codeCoverageIgnoreStart
164
        if (null === self::$height) {
165
            self::initDimensions();
166
        }
167
        return self::$height ?: static::DEFAULT_HEIGHT;
168
        // @codeCoverageIgnoreEnd
169
    }
170
171
    /**
172
     * @return bool
173
     */
174 1
    public function supports256Color(): bool
175
    {
176 1
        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...
177
            return static::$supports256Color;
178
        }
179
        return
180 1
            static::$supports256Color = $this->check256ColorSupport();
181
    }
182
183
    /**
184
     * @return bool
185
     */
186 1
    public function supportsColor(): bool
187
    {
188 1
        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...
189
            return static::$supportsColor;
190
        }
191
        return
192 1
            static::$supportsColor = $this->checkColorSupport();
193
    }
194
195
    /**
196
     * @return bool
197
     */
198 1
    protected function check256ColorSupport(): bool
199
    {
200 1
        if (DIRECTORY_SEPARATOR === '\\') {
201
            return
202
                \function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support(STDOUT);
203
        }
204 1
        if (!$terminal = \getenv('TERM')) {
205
            // @codeCoverageIgnoreStart
206
            return false;
207
            // @codeCoverageIgnoreEnd
208
        }
209 1
        return \strpos($terminal, '256color') !== false;
210
    }
211
212
    /**
213
     * @return bool
214
     */
215 1
    protected function checkColorSupport(): bool
216
    {
217 1
        if (DIRECTORY_SEPARATOR === '\\') {
218
            if (\function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support(STDOUT)) {
219
                return true;
220
            }
221
            if (\getenv('ANSICON') !== false || \getenv('ConEmuANSI') === 'ON') {
222
                return true;
223
            }
224
            return false;
225
        }
226
        /** @noinspection PhpComposerExtensionStubsInspection */
227 1
        return \function_exists('posix_isatty') && @\posix_isatty(STDOUT);
228
    }
229
}
230