Passed
Push — master ( e2bdc6...71496d )
by Alec
03:05
created

Spinner::spin()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 5
c 8
b 0
f 0
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\Spinner\Core;
4
5
use AlecRabbit\Cli\Tools\Cursor;
6
use AlecRabbit\Spinner\Core\Contracts\SpinnerInterface;
7
use AlecRabbit\Spinner\Core\Contracts\SpinnerOutputInterface;
8
use AlecRabbit\Spinner\Core\Jugglers\Contracts\JugglerInterface;
9
use AlecRabbit\Spinner\Core\Jugglers\FrameJuggler;
10
use AlecRabbit\Spinner\Core\Jugglers\MessageJuggler;
11
use AlecRabbit\Spinner\Core\Jugglers\ProgressJuggler;
12
use AlecRabbit\Spinner\Settings\Contracts\Defaults;
13
use AlecRabbit\Spinner\Settings\Settings;
14
use function AlecRabbit\typeOf;
15
use const AlecRabbit\ESC;
16
17
abstract class Spinner extends SpinnerCore
18
{
19
    /** @var Settings */
20
    protected $settings;
21
    /** @var bool */
22
    protected $inline = false;
23
    /** @var float */
24
    protected $interval;
25
    /** @var null|MessageJuggler */
26
    protected $messageJuggler;
27
    /** @var null|FrameJuggler */
28
    protected $frameJuggler;
29
    /** @var null|ProgressJuggler */
30
    protected $progressJuggler;
31
    /** @var string */
32
    protected $moveCursorBackSequence = '';
33
    /** @var string */
34
    protected $eraseBySpacesSequence = '';
35
    /** @var int */
36
    protected $previousErasingLength = 0;
37
    /** @var string */
38
    protected $spacer = Defaults::EMPTY_STRING;
39
    /** @var Coloring */
40
    protected $coloring;
41
    /** @var null[]|JugglerInterface[] */
42
    protected $jugglers = [];
43
44
    /**
45
     * Spinner constructor.
46
     *
47
     * @param null|string|Settings $messageOrSettings
48
     * @param null|false|SpinnerOutputInterface $output
49
     * @param null|int $color
50
     */
51 29
    public function __construct($messageOrSettings = null, $output = null, ?int $color = null)
52
    {
53 29
        $this->output = $this->refineOutput($output);
54 27
        $this->settings = $this->refineSettings($messageOrSettings);
55 26
        $this->interval = $this->settings->getInterval();
56
        try {
57 26
            $this->coloring = new Coloring($this->settings->getStyles(), $color);
58 1
        } catch (\Throwable $e) {
59 1
            throw new \InvalidArgumentException(
60 1
                '[' . static::class . '] ' . $e->getMessage(),
61 1
                (int)$e->getCode(),
62 1
                $e
63
            );
64
        }
65 25
        $this->initJugglers();
66 25
        $this->jugglers = [
67 25
            &$this->frameJuggler,
68 25
            &$this->messageJuggler,
69 25
            &$this->progressJuggler,
70
        ];
71 25
    }
72
73
74
    /**
75
     * @param null|string|Settings $settings
76
     * @return Settings
77
     */
78 27
    protected function refineSettings($settings): Settings
79
    {
80 27
        $this->assertSettings($settings);
81 26
        if (\is_string($settings)) {
82
            return
83 18
                $this->defaultSettings()->setMessage($settings);
84
        }
85 8
        if ($settings instanceof Settings) {
86 5
            return $this->defaultSettings()->merge($settings);
87
        }
88
        return
89 3
            $this->defaultSettings();
90
    }
91
92
    /**
93
     * @param mixed $settings
94
     */
95 27
    protected function assertSettings($settings): void
96
    {
97 27
        if (null !== $settings && !\is_string($settings) && !$settings instanceof Settings) {
98 1
            throw new \InvalidArgumentException(
99 1
                'Instance of [' . Settings::class . '] or string expected ' . typeOf($settings) . ' given.'
100
            );
101
        }
102 26
    }
103
104
    /**
105
     * @return Settings
106
     */
107 26
    protected function defaultSettings(): Settings
108
    {
109
        return
110 26
            (new Settings())
111 26
                ->setInterval(static::INTERVAL)
112 26
                ->setFrames(static::FRAMES)
113 26
                ->setStyles(static::STYLES);
114
    }
115
116 25
    protected function initJugglers(): void
117
    {
118 25
        $frames = $this->settings->getFrames();
119 25
        if (!empty($frames)) {
120 19
            $this->frameJuggler =
121 19
                new FrameJuggler($frames, $this->coloring->getFrameStyles());
122
        }
123
124 25
        $message = $this->settings->getMessage();
125 25
        if (Defaults::EMPTY_STRING !== $message) {
126 21
            $this->setMessage($message, $this->settings->getMessageErasingLen());
127
        }
128 25
    }
129
130
    /**
131
     * @param null|string $message
132
     * @param null|int $erasingLength
133
     */
134 22
    protected function setMessage(?string $message, ?int $erasingLength = null): void
135
    {
136 22
        if ($this->messageJuggler instanceof MessageJuggler) {
137 5
            if (null === $message) {
138 1
                $this->messageJuggler = null;
139
            } else {
140 5
                $this->messageJuggler->setMessage($message, $erasingLength);
141
            }
142
        } else {
143 22
            $this->messageJuggler =
144 22
                null === $message ?
145 1
                    null :
146 22
                    new MessageJuggler($message, $erasingLength, $this->coloring->getMessageStyles());
147
        }
148 22
    }
149
150
    /** {@inheritDoc} */
151 21
    public function end(): string
152
    {
153 21
        if ($this->output instanceof SpinnerOutputInterface) {
154 3
            $this->erase();
155 3
            $this->output->write(Cursor::show());
156 3
            return self::EMPTY_STRING;
157
        }
158 18
        return $this->erase() . Cursor::show();
159
    }
160
161
    /** {@inheritDoc} */
162 21
    public function erase(): string
163
    {
164 21
        $str = $this->eraseBySpacesSequence . $this->moveCursorBackSequence;
165 21
        if ($this->output instanceof SpinnerOutputInterface) {
166 3
            $this->output->write($str);
167 3
            return self::EMPTY_STRING;
168
        }
169 18
        return $str;
170
    }
171
172
    /** {@inheritDoc} */
173 3
    public function getOutput(): ?SpinnerOutputInterface
174
    {
175 3
        return $this->output;
176
    }
177
178 8
    public function inline(bool $inline): SpinnerInterface
179
    {
180 8
        $this->inline = $inline;
181 8
        $this->spacer = $this->inline ? Defaults::ONE_SPACE_SYMBOL : Defaults::EMPTY_STRING;
182 8
        return $this;
183
    }
184
185 5
    public function interval(): float
186
    {
187 5
        return $this->interval;
188
    }
189
190
    /** {@inheritDoc} */
191 5
    public function message(?string $message = null, ?int $erasingLength = null): self
192
    {
193 5
        $this->setMessage($message, $erasingLength);
194 5
        return $this;
195
    }
196
197 10
    public function progress(?float $percent): self
198
    {
199 10
        $this->setProgress($percent);
200 10
        return $this;
201
    }
202
203
    /**
204
     * @param null|float $percent
205
     */
206 11
    protected function setProgress(?float $percent = null): void
207
    {
208 11
        if ($this->progressJuggler instanceof ProgressJuggler) {
209 10
            if (null === $percent) {
210 1
                $this->progressJuggler = null;
211
            } else {
212 10
                $this->progressJuggler->setProgress($percent);
213
            }
214
        } else {
215 11
            $this->progressJuggler =
216 11
                null === $percent ? null : new ProgressJuggler($percent, $this->coloring->getProgressStyles());
217
        }
218 11
    }
219
220
    /** {@inheritDoc} */
221 24
    public function begin(?float $percent = null): string
222
    {
223 24
        if (null === $percent) {
224 20
            $this->progressJuggler = null;
225
        } else {
226 5
            $this->setProgress($percent);
227
        }
228 24
        if ($this->output instanceof SpinnerOutputInterface) {
229 3
            $this->output->write(Cursor::hide());
230 3
            $this->spin();
231 3
            return self::EMPTY_STRING;
232
        }
233 21
        return Cursor::hide() . $this->spin();
234
    }
235
236
    /** {@inheritDoc} */
237 24
    public function spin(): string
238
    {
239 24
        if ($this->output instanceof SpinnerOutputInterface) {
240 3
            $this->output->write($this->preparedStr());
241 3
            return self::EMPTY_STRING;
242
        }
243
        return
244 21
            $this->preparedStr();
245
    }
246
247 24
    protected function preparedStr(): string
248
    {
249
//        $start = hrtime(true);
250 24
        $str = '';
251 24
        $erasingLength = 0;
252 24
        $eraseTailBySpacesSequence = '';
253 24
        foreach ($this->jugglers as $juggler) {
254 24
            if ($juggler instanceof JugglerInterface) {
255 23
                $str .= $juggler->getStyledFrame();
256 23
                $erasingLength += $juggler->getFrameErasingLength();
257
            }
258
        }
259 24
        $erasingLength += $this->inline ? 1 : 0;
260 24
        $erasingLengthDelta = $this->previousErasingLength - $erasingLength;
261 24
        $this->previousErasingLength = $erasingLength;
262
263 24
        if ($erasingLengthDelta > 0) {
264 6
            $erasingLength += $erasingLengthDelta;
265 6
            $eraseTailBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingLengthDelta);
266
        }
267 24
        $this->moveCursorBackSequence = ESC . "[{$erasingLength}D";
268 24
        $this->eraseBySpacesSequence = str_repeat(Defaults::ONE_SPACE_SYMBOL, $erasingLength);
269
270 24
        $str = $this->spacer . $str . $eraseTailBySpacesSequence . $this->moveCursorBackSequence;
271
//        dump(hrtime(true) - $start);
272 24
        return $str;
273
    }
274
}
275