Passed
Push — master ( 1920c4...e429c0 )
by Alec
02:41
created

ConsoleColor::isSupported()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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