Passed
Push — master ( 18055d...16186d )
by Alec
14:52
created

ConsoleColor::setStream()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 10.3999

Importance

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