Passed
Push — master ( 2b6410...1d5ce8 )
by Alec
03:54 queued 01:07
created

ConsoleColor   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Test Coverage

Coverage 88.68%

Importance

Changes 0
Metric Value
eloc 132
dl 0
loc 381
ccs 94
cts 106
cp 0.8868
rs 7.92
c 0
b 0
f 0
wmc 51

26 Methods

Rating   Name   Duplication   Size   Complexity  
A removeTheme() 0 3 1
A isStyleForced() 0 3 1
A assertStyles() 0 4 2
A applySequences() 0 6 1
A styleSequence() 0 12 3
A checkUnix() 0 3 2
A refineStyles() 0 7 2
A setThemes() 0 5 2
A apply() 0 16 4
A are256ColorsUnix() 0 8 2
A escSequence() 0 3 1
A checkIfTerminalColorIsSupported() 0 6 2
A are256ColorsWindows() 0 4 2
A checkWindows() 0 9 5
A getSequencesFrom() 0 28 4
A process256ColorStyle() 0 8 2
A getPossibleStyles() 0 3 1
A isValidStyle() 0 3 2
A are256ColorsSupported() 0 6 2
A setForceStyle() 0 3 1
A hasTheme() 0 3 1
A themeSequence() 0 7 2
A isSupported() 0 3 1
A getThemes() 0 3 1
A __construct() 0 3 1
A addTheme() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like ConsoleColor 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 ConsoleColor, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\ConsoleColour;
4
5
use JakubOnderka\PhpConsoleColor\InvalidStyleException;
6
7
class ConsoleColor
8
{
9
    public const FOREGROUND = 38;
10
    public const BACKGROUND = 48;
11
    public const RESET_STYLE = 0;
12
    public const COLOR256_REGEXP = '~^(bg_)?color_(\d{1,3})$~';
13
14
    protected const STYLES =
15
        [
16
            'none' => null,
17
            'bold' => '1',
18
            'dark' => '2',
19
            'italic' => '3',
20
            'underline' => '4',
21
            'blink' => '5',
22
            'reverse' => '7',
23
            'concealed' => '8',
24
25
            'default' => '39',
26
            'black' => '30',
27
            'red' => '31',
28
            'green' => '32',
29
            'yellow' => '33',
30
            'blue' => '34',
31
            'magenta' => '35',
32
            'cyan' => '36',
33
            'light_gray' => '37',
34
35
            'dark_gray' => '90',
36
            'light_red' => '91',
37
            'light_green' => '92',
38
            'light_yellow' => '93',
39
            'light_blue' => '94',
40
            'light_magenta' => '95',
41
            'light_cyan' => '96',
42
            'white' => '97',
43
44
            'bg_default' => '49',
45
            'bg_black' => '40',
46
            'bg_red' => '41',
47
            'bg_green' => '42',
48
            'bg_yellow' => '43',
49
            'bg_blue' => '44',
50
            'bg_magenta' => '45',
51
            'bg_cyan' => '46',
52
            'bg_light_gray' => '47',
53
54
            'bg_dark_gray' => '100',
55
            'bg_light_red' => '101',
56
            'bg_light_green' => '102',
57
            'bg_light_yellow' => '103',
58
            'bg_light_blue' => '104',
59
            'bg_light_magenta' => '105',
60
            'bg_light_cyan' => '106',
61
            'bg_white' => '107',
62
        ];
63
64
    /** @var bool */
65
    protected $isSupported;
66
    /** @var bool */
67
    protected $forceStyle = false;
68
    /** @var array */
69
    protected $themes = [];
70
71 70
    public function __construct()
72
    {
73 70
        $this->isSupported = $this->checkIfTerminalColorIsSupported();
74 70
    }
75
76
    /**
77
     * @return bool
78
     */
79 70
    public function checkIfTerminalColorIsSupported(): bool
80
    {
81 70
        if (DIRECTORY_SEPARATOR === '\\') {
82
            return $this->checkWindows();
83
        }
84 70
        return $this->checkUnix();
85
    }
86
87
    /**
88
     * @return bool
89
     */
90
    protected function checkWindows(): bool
91
    {
92
        if (\function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support(STDOUT)) {
93
            return true;
94
        }
95
        if (\getenv('ANSICON') !== false || \getenv('ConEmuANSI') === 'ON') {
96
            return true;
97
        }
98
        return false;
99
    }
100
101
    /**
102
     * @return bool
103
     */
104 70
    protected function checkUnix(): bool
105
    {
106 70
        return \function_exists('posix_isatty') && @\posix_isatty(STDOUT);
107
    }
108
109
    /**
110
     * @param string|array $styles
111
     * @param string $text
112
     * @return string
113
     * @throws InvalidStyleException
114
     * @throws \InvalidArgumentException
115
     */
116 54
    public function apply($styles, $text): string
117
    {
118 54
        if (!$this->isStyleForced() && !$this->isSupported()) {
119 3
            return $text;
120
        }
121
122
        $sequences =
123 51
            $this->getSequencesFrom(
124 51
                $this->refineStyles($styles)
125
            );
126
127 42
        if (empty($sequences)) {
128 6
            return $text;
129
        }
130
131 36
        return $this->applySequences($text, $sequences);
132
    }
133
134
    /**
135
     * @return bool
136
     */
137 57
    public function isStyleForced(): bool
138
    {
139 57
        return $this->forceStyle;
140
    }
141
142
    /**
143
     * @return bool
144
     */
145 1
    public function isSupported(): bool
146
    {
147 1
        return $this->isSupported;
148
    }
149
150
    /**
151
     * @param array $styles
152
     * @return array
153
     * @throws InvalidStyleException
154
     */
155 48
    protected function getSequencesFrom(array $styles): array
156
    {
157 48
        $sequences = [[]];
158
159 48
        foreach ($styles as $s) {
160 48
            if (isset($this->themes[$s])) {
161 15
                $sequences[] = $this->themeSequence($s);
162 37
            } elseif ($this->isValidStyle($s)) {
163 31
                $sequences[][] = $this->styleSequence($s);
164
            } else {
165 48
                throw new InvalidStyleException($s);
166
            }
167
        }
168
169
        $sequences =
170 42
            \array_filter(
171 42
                \array_merge(...$sequences),
172
                /**
173
                 * @param mixed $val
174
                 * @return bool
175
                 * @psalm-suppress MissingClosureParamType
176
                 */
177
                function ($val): bool {
178 42
                    return $val !== null;
179 42
                }
180
            );
181
182 42
        return $sequences;
183
    }
184
185
    /**
186
     * @param string $name
187
     * @return string[]|null[]
188
     */
189 15
    protected function themeSequence($name): array
190
    {
191 15
        $sequences = [];
192 15
        foreach ($this->themes[$name] as $style) {
193 15
            $sequences[] = $this->styleSequence($style);
194
        }
195 15
        return $sequences;
196
    }
197
198
    /**
199
     * @param string $style
200
     * @return null|string
201
     */
202 42
    protected function styleSequence($style): ?string
203
    {
204 42
        if (\array_key_exists($style, static::STYLES)) {
205 30
            return static::STYLES[$style];
206
        }
207
208 12
        if (!$this->are256ColorsSupported()) {
209 3
            return null;
210
        }
211
212
        return
213 9
            $this->process256ColorStyle($style);
214
    }
215
216
    /**
217
     * @return bool
218
     */
219 2
    public function are256ColorsSupported(): bool
220
    {
221 2
        if (DIRECTORY_SEPARATOR === '\\') {
222
            return $this->are256ColorsWindows();
223
        }
224 2
        return $this->are256ColorsUnix();
225
    }
226
227
    /**
228
     * @return bool
229
     */
230
    protected function are256ColorsWindows(): bool
231
    {
232
        return
233
            \function_exists('sapi_windows_vt100_support') && @\sapi_windows_vt100_support(STDOUT);
234
    }
235
236
    /**
237
     * @return bool
238
     */
239 2
    protected function are256ColorsUnix(): bool
240
    {
241 2
        if (!$terminal = \getenv('TERM')) {
242
            // @codeCoverageIgnoreStart
243
            return false;
244
            // @codeCoverageIgnoreEnd
245
        }
246 2
        return \strpos($terminal, '256color') !== false;
247
    }
248
249
    /**
250
     * @param string $style
251
     * @return bool
252
     */
253 56
    protected function isValidStyle($style): bool
254
    {
255 56
        return \array_key_exists($style, static::STYLES) || \preg_match(self::COLOR256_REGEXP, $style);
256
    }
257
258
    /**
259
     * @param string|array $styles
260
     * @return array
261
     */
262 59
    protected function refineStyles($styles): array
263
    {
264 59
        if (\is_string($styles)) {
265 35
            $styles = [$styles];
266
        }
267 59
        $this->assertStyles($styles);
268 56
        return $styles;
269
    }
270
271
    /**
272
     * @param mixed $styles
273
     */
274 59
    protected function assertStyles($styles): void
275
    {
276 59
        if (!\is_array($styles)) {
277 3
            throw new \InvalidArgumentException('Style must be string or array.');
278
        }
279 56
    }
280
281
    /**
282
     * @param string $text
283
     * @param array $sequences
284
     * @return string
285
     */
286 36
    protected function applySequences(string $text, array $sequences): string
287
    {
288
        return
289 36
            $this->escSequence(\implode(';', $sequences)) .
290 36
            $text .
291 36
            $this->escSequence((string)self::RESET_STYLE);
292
    }
293
294
    /**
295
     * @param string $value
296
     * @return string
297
     */
298 36
    protected function escSequence(string $value): string
299
    {
300 36
        return "\033[{$value}m";
301
    }
302
303
    /**
304
     * @param bool $forceStyle
305
     */
306 6
    public function setForceStyle(bool $forceStyle): void
307
    {
308 6
        $this->forceStyle = $forceStyle;
309 6
    }
310
311
    /**
312
     * @return array
313
     */
314
    public function getThemes(): array
315
    {
316
        return $this->themes;
317
    }
318
319
    /**
320
     * @param array $themes
321
     * @throws InvalidStyleException
322
     * @throws \InvalidArgumentException
323
     */
324 3
    public function setThemes(array $themes): void
325
    {
326 3
        $this->themes = [];
327 3
        foreach ($themes as $name => $styles) {
328 3
            $this->addTheme($name, $styles);
329
        }
330 3
    }
331
332
    /**
333
     * @param string $name
334
     * @param array|string $styles
335
     * @throws \InvalidArgumentException
336
     * @throws InvalidStyleException
337
     */
338 23
    public function addTheme($name, $styles): void
339
    {
340 23
        $styles = $this->refineStyles($styles);
341
342 23
        foreach ($styles as $style) {
343 23
            if (!$this->isValidStyle($style)) {
344 23
                throw new InvalidStyleException($style);
345
            }
346
        }
347
348 20
        $this->themes[$name] = $styles;
349 20
    }
350
351
    /**
352
     * @param string $name
353
     * @return bool
354
     */
355 3
    public function hasTheme($name): bool
356
    {
357 3
        return isset($this->themes[$name]);
358
    }
359
360
    /**
361
     * @param string $name
362
     */
363 3
    public function removeTheme($name): void
364
    {
365 3
        unset($this->themes[$name]);
366 3
    }
367
368
    /**
369
     * @return array
370
     */
371 3
    public function getPossibleStyles(): array
372
    {
373 3
        return \array_keys(static::STYLES);
374
    }
375
376
    /**
377
     * @param string $style
378
     * @return string
379
     */
380 9
    protected function process256ColorStyle(string $style): string
381
    {
382 9
        \preg_match(self::COLOR256_REGEXP, $style, $matches);
383
384 9
        $type = $matches[1] === 'bg_' ? self::BACKGROUND : self::FOREGROUND;
385 9
        $value = $matches[2];
386
387 9
        return "$type;5;$value";
388
    }
389
}
390