Test Setup Failed
Branch master (816c76)
by Alec
04:22
created

AbstractTerminal   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 261
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 102
dl 0
loc 261
rs 8.8798
c 0
b 0
f 0
wmc 44

11 Methods

Rating   Name   Duplication   Size   Complexity  
A initDimensionsWindows() 0 12 5
A initDimensions() 0 6 3
A getConsoleMode() 0 23 5
A getHeight() 0 11 4
B hasColorSupport() 0 31 10
A check256ColorSupport() 0 7 3
A checkFor256ColorSupport() 0 7 2
A getWidth() 0 11 4
A getSttyColumns() 0 29 4
A initDimensionsUnix() 0 10 3
A onWindows() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractTerminal often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractTerminal, and based on these observations, apply Extract Interface, too.

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
abstract class AbstractTerminal
18
{
19
    protected const DEFAULT_WIDTH = 80;
20
    protected const DEFAULT_HEIGHT = 50;
21
    protected const ENV_TERM = 'TERM';
22
    protected const ENV_ANSICON = 'ANSICON';
23
    protected const ENV_CON_EMU_ANSI = 'ConEmuANSI';
24
    protected const ENV_DOCKER_TERM = 'DOCKER_TERM';
25
    protected const COLOR_NEEDLE = '256color';
26
    protected const ENV_COLUMNS = 'COLUMNS';
27
    protected const ENV_LINES = 'LINES';
28
    protected const ENV_TERM_PROGRAM = 'TERM_PROGRAM';
29
30
    /** @var null|int */
31
    protected static $width;
32
33
    /** @var null|int */
34
    protected static $height;
35
36
    /** @var null|bool */
37
    protected static $supports256Color;
38
39
    /** @var null|bool */
40
    protected static $supportsColor;
41
42
    /**
43
     * Gets the terminal width.
44
     *
45
     * @return int
46
     */
47
    protected function getWidth(): int
48
    {
49
        $width = getenv(static::ENV_COLUMNS);
50
        if (false !== $width) {
51
            return (int)trim($width);
52
        }
53
        // @codeCoverageIgnoreStart
54
        if (null === static::$width) {
55
            static::initDimensions();
56
        }
57
        return static::$width ?: static::DEFAULT_WIDTH;
58
        // @codeCoverageIgnoreEnd
59
    }
60
61
    /**
62
     * @codeCoverageIgnore
63
     */
64
    protected static function initDimensions(): void
65
    {
66
        if (static::onWindows()) {
67
            self::initDimensionsWindows();
68
        } elseif ($sttyString = static::getSttyColumns()) {
69
            self::initDimensionsUnix($sttyString);
70
        }
71
    }
72
73
    /**
74
     * @return bool
75
     */
76
    protected static function onWindows(): bool
77
    {
78
        return '\\' === \DIRECTORY_SEPARATOR;
79
    }
80
81
    /**
82
     * @codeCoverageIgnore
83
     */
84
    protected static function initDimensionsWindows(): void
85
    {
86
        if ((false !== $term = getenv(static::ENV_ANSICON)) &&
87
            preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim($term), $matches)) {
88
            // extract [w, H] from "wxh (WxH)"
89
            // or [w, h] from "wxh"
90
            static::$width = (int)$matches[1];
91
            static::$height = isset($matches[4]) ? (int)$matches[4] : (int)$matches[2];
92
        } elseif (null !== $dimensions = static::getConsoleMode()) {
93
            // extract [w, h] from "wxh"
94
            static::$width = (int)$dimensions[0];
95
            static::$height = (int)$dimensions[1];
96
        }
97
    }
98
99
    /**
100
     * @codeCoverageIgnore
101
     *
102
     * Runs and parses mode CON if it's available, suppressing any error output.
103
     *
104
     * @return int[]|null An array composed of the width and the height or null if it could not be parsed
105
     */
106
    protected static function getConsoleMode(): ?array
107
    {
108
        if (!\function_exists('proc_open')) {
109
            return null;
110
        }
111
112
        $descriptorSpec = [
113
            1 => ['pipe', 'w'],
114
            2 => ['pipe', 'w'],
115
        ];
116
        $process =
117
            proc_open('mode CON', $descriptorSpec, $pipes, null, null, ['suppress_errors' => true]);
118
        if (\is_resource($process)) {
119
            $info = stream_get_contents($pipes[1]);
120
            fclose($pipes[1]);
121
            fclose($pipes[2]);
122
            proc_close($process);
123
124
            if (false !== $info && preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) {
125
                return [(int)$matches[2], (int)$matches[1]];
126
            }
127
        }
128
        return null;
129
    }
130
131
    /**
132
     * @codeCoverageIgnore
133
     *
134
     * Runs and parses stty -a if it's available, suppressing any error output.
135
     *
136
     * @return string|null
137
     */
138
    protected static function getSttyColumns(): ?string
139
    {
140
        if (!\function_exists('proc_open')) {
141
            return null;
142
        }
143
144
        $descriptorSpec = [
145
            1 => ['pipe', 'w'],
146
            2 => ['pipe', 'w'],
147
        ];
148
149
        $process = proc_open(
150
            'stty -a | grep columns',
151
            $descriptorSpec,
152
            $pipes,
153
            null,
154
            null,
155
            ['suppress_errors' => true]
156
        );
157
158
        if (\is_resource($process)) {
159
            $info = stream_get_contents($pipes[1]);
160
            fclose($pipes[1]);
161
            fclose($pipes[2]);
162
            proc_close($process);
163
164
            return $info ?: null;
165
        }
166
        return null;
167
    }
168
169
    /**
170
     * @codeCoverageIgnore
171
     * @param string $sttyString
172
     */
173
    protected static function initDimensionsUnix(string $sttyString): void
174
    {
175
        if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
176
            // extract [w, h] from "rows h; columns w;"
177
            static::$width = (int)$matches[2];
178
            static::$height = (int)$matches[1];
179
        } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
180
            // extract [w, h] from "; h rows; w columns"
181
            static::$width = (int)$matches[2];
182
            static::$height = (int)$matches[1];
183
        }
184
    }
185
186
    /**
187
     * Gets the terminal height.
188
     *
189
     * @return int
190
     */
191
    protected function getHeight(): int
192
    {
193
        $height = getenv(static::ENV_LINES);
194
        if (false !== $height) {
195
            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
    protected function check256ColorSupport(): bool
209
    {
210
        return
211
            $this->supportsColor() ?
0 ignored issues
show
Bug introduced by
The method supportsColor() does not exist on AlecRabbit\ConsoleColour\AbstractTerminal. Since it exists in all sub-types, consider adding an abstract or default implementation to AlecRabbit\ConsoleColour\AbstractTerminal. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

211
            $this->/** @scrutinizer ignore-call */ 
212
                   supportsColor() ?
Loading history...
212
                $this->checkFor256ColorSupport(static::ENV_TERM) ||
213
                $this->checkFor256ColorSupport(static::ENV_DOCKER_TERM) :
214
                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
    protected function hasColorSupport(): bool
234
    {
235
        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
        if (\function_exists('stream_isatty')) {
252
            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
    protected function checkFor256ColorSupport(string $varName): bool
272
    {
273
        if ($t = getenv($varName)) {
274
            return
275
                false !== strpos($t, static::COLOR_NEEDLE);
276
        }
277
        return false;
278
    }
279
}
280