Passed
Push — master ( 095b07...d08e35 )
by Alec
02:42
created

Spinner::progress()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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