ConsoleColor::setThemes()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 2
rs 10
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\ConsoleColour;
4
5
use AlecRabbit\Cli\Tools\Core\Terminal;
6
use AlecRabbit\ConsoleColour\Contracts\Styles;
7
use AlecRabbit\ConsoleColour\Core\Contracts\ConsoleColorInterface;
8
use AlecRabbit\ConsoleColour\Exception\InvalidStyleException;
9
use const AlecRabbit\COLOR256_TERMINAL;
10
use const AlecRabbit\COLOR_TERMINAL;
11
use const AlecRabbit\CSI;
12
use const AlecRabbit\NO_COLOR_TERMINAL;
13
use const AlecRabbit\TRUECOLOR_TERMINAL;
14
15
class ConsoleColor implements ConsoleColorInterface
16
{
17
    /** @var bool */
18
    protected $supported;
19
    /** @var bool */
20
    protected $forced;
21
    /** @var array */
22
    protected $themes = [];
23
    /** @var int */
24
    protected $colorLevel;
25
26
    /**
27
     * ConsoleColor constructor.
28
     * @param null|bool|resource $stream
29
     * @param null|int $colorLevel
30
     */
31 31
    public function __construct($stream = null, ?int $colorLevel = null)
32
    {
33 31
        $this->colorLevel = $this->refineColorLevel($stream, $colorLevel);
34 31
    }
35
36
    /**
37
     * @param mixed $stream
38
     * @param null|int $colorLevel
39
     * @return int
40
     */
41 31
    protected function refineColorLevel($stream, ?int $colorLevel): int
42
    {
43 31
        $colorSupport = Terminal::colorSupport($stream);
44 31
        if (null === $colorLevel) {
45 2
            $this->supported = $colorSupport >= COLOR_TERMINAL;
46 2
            $this->forced = false;
47 2
            return $colorSupport;
48
        }
49 30
        $this->supported = $colorSupport >= $colorLevel;
50 30
        $this->forced = $colorSupport < $colorLevel;
51 30
        if (NO_COLOR_TERMINAL === $colorLevel) {
52 20
            $this->supported = false;
53
        }
54 30
        return $colorLevel;
55
    }
56
57
    /**
58
     * @return int
59
     */
60 6
    public function getColorLevel(): int
61
    {
62 6
        return $this->colorLevel;
63
    }
64
65
    /** {@inheritdoc} */
66 16
    public function apply($styles, $text): string
67
    {
68 16
        if (!$this->isApplicable()) {
69 13
            return $text;
70
        }
71
72
        $sequences =
73 15
            $this->getSequencesFrom(
74 15
                $this->refineStyles($styles)
75
            );
76
77 12
        if (empty($sequences)) {
78 3
            return $text;
79
        }
80
81 10
        return $this->applySequences($text, $sequences);
82
    }
83
84
    /** {@inheritdoc} */
85 26
    public function isApplicable(): bool
86
    {
87 26
        return $this->supported || $this->forced;
88
    }
89
90
//    /** {@inheritdoc} */
91
//    public function isSupported(?int $colorLevel = null): bool
92
//    {
93
//        return $this->supported || $this->forced;
94
//    }
95
//
96
    /**
97
     * @param array $styles
98
     * @return array
99
     * @throws InvalidStyleException
100
     */
101 14
    protected function getSequencesFrom(array $styles): array
102
    {
103 14
        $sequences = [[]];
104
105 14
        foreach ($styles as $style) {
106 13
            if (isset($this->themes[$style])) {
107 2
                $sequences[] = $this->themeSequence($style);
108 13
            } elseif ($this->isValid($style)) {
109 11
                $sequences[][] = $this->styleSequence($style);
110
            } else {
111 2
                throw new InvalidStyleException($style);
112
            }
113
        }
114
115
        return
116 12
            array_filter(
117 12
                array_merge(...$sequences),
118
                /**
119
                 * @param mixed $val
120
                 * @return bool
121
                 * @psalm-suppress MissingClosureParamType
122
                 */
123
                static function ($val): bool {
124 11
                    return $val !== null;
125 12
                }
126
            );
127
    }
128
129
    /**
130
     * @param string $name
131
     * @return string[]|null[]
132
     */
133 2
    protected function themeSequence($name): array
134
    {
135 2
        $sequences = [];
136 2
        foreach ($this->themes[$name] as $style) {
137 2
            $sequences[] = $this->styleSequence($style);
138
        }
139 2
        return $sequences;
140
    }
141
142
    /**
143
     * @param string $style
144
     * @return null|string
145
     */
146 11
    protected function styleSequence($style): ?string
147
    {
148 11
        if (\array_key_exists($style, static::CODES)) {
149 10
            return static::CODES[$style];
150
        }
151
152 3
        if (!$this->are256ColorsSupported()) {
153 3
            return null;
154
        }
155
156
        return
157 3
            $this->process256ColorStyle($style);
158
    }
159
160
    /** {@inheritdoc} */
161 4
    public function are256ColorsSupported(): bool
162
    {
163 4
        return $this->colorLevel >= COLOR256_TERMINAL;
164
    }
165
166
    /**
167
     * @param string $style
168
     * @return string
169
     */
170 3
    protected function process256ColorStyle(string $style): string
171
    {
172 3
        preg_match(self::COLOR256_REGEXP, $style, $matches);
173
        return
174 3
            sprintf(
175 3
                '%s;5;%s',
176 3
                $matches[1] === self::BG ? self::BACKGROUND : self::FOREGROUND,
177 3
                $matches[2]
178
            );
179
    }
180
181
    /**
182
     * @param string $style
183
     * @return bool
184
     */
185 23
    protected function isValid($style): bool
186
    {
187
        return
188 23
            \array_key_exists($style, static::CODES) || (bool)preg_match(self::COLOR256_REGEXP, $style);
189
    }
190
191
    /**
192
     * @param mixed $styles
193
     * @return array
194
     */
195 25
    protected function refineStyles($styles): array
196
    {
197 25
        if (\is_int($styles) || \is_string($styles)) {
198 13
            $styles = [$styles];
199
        }
200 25
        $this->assertStyles($styles);
201 24
        return $styles;
202
    }
203
204
    /**
205
     * @param mixed $styles
206
     */
207 25
    protected function assertStyles($styles): void
208
    {
209 25
        if (!\is_array($styles)) {
210 1
            throw new \InvalidArgumentException('Styles must be type of int, string or array.');
211
        }
212 24
    }
213
214
    /**
215
     * @param string $text
216
     * @param array $sequences
217
     * @return string
218
     */
219 10
    protected function applySequences(string $text, array $sequences): string
220
    {
221
        return
222 10
            $this->escSequence(implode(';', $sequences)) .
223 10
            $text .
224 10
            $this->escSequence((string)self::RESET);
225
    }
226
227
    /**
228
     * @param string $value
229
     * @return string
230
     */
231 10
    protected function escSequence(string $value): string
232
    {
233
        return
234 10
            CSI . $value . 'm';
235
    }
236
237
    /** {@inheritdoc} */
238 2
    public function isTrueColorSupported(): bool
239
    {
240 2
        return $this->colorLevel >= TRUECOLOR_TERMINAL;
241
    }
242
243
    /** {@inheritdoc} */
244 7
    public function isForced(): bool
245
    {
246 7
        return $this->forced;
247
    }
248
249
    /** {@inheritdoc} */
250 1
    public function getThemes(): array
251
    {
252 1
        return $this->themes;
253
    }
254
255
    /** {@inheritdoc} */
256 1
    public function setThemes(array $themes): void
257
    {
258 1
        $this->themes = [];
259 1
        foreach ($themes as $name => $styles) {
260 1
            $this->addTheme($name, $styles);
261
        }
262 1
    }
263
264
    /** {@inheritdoc} */
265 14
    public function addTheme($name, $styles, bool $override = false): void
266
    {
267 14
        if (\array_key_exists($name, $this->themes) && false === $override) {
268 1
            throw new \RuntimeException('Theme [' . $name . '] is already set.');
269
        }
270
271 14
        $styles = $this->refineStyles($styles);
272
273 14
        foreach ($styles as $style) {
274 14
            if (!$this->isValid($style)) {
275 4
                throw new InvalidStyleException($style);
276
            }
277
        }
278 10
        $this->themes[$name] = $styles;
279 10
    }
280
281
    /** {@inheritdoc} */
282 1
    public function hasTheme($name): bool
283
    {
284 1
        return isset($this->themes[$name]);
285
    }
286
287
    /** {@inheritdoc} */
288 1
    public function removeTheme($name): void
289
    {
290 1
        unset($this->themes[$name]);
291 1
    }
292
293
    /** {@inheritdoc} */
294 1
    public function getPossibleStyles(): array
295
    {
296 1
        return array_keys(Styles::NAMES);
297
    }
298
299
    /**
300
     * @return bool
301
     */
302 7
    public function isSupported(): bool
303
    {
304 7
        return $this->supported;
305
    }
306
}
307