1
|
|
|
<?php declare(strict_types=1); |
2
|
|
|
|
3
|
|
|
namespace AlecRabbit\Spinner\Core; |
4
|
|
|
|
5
|
|
|
use AlecRabbit\Accessories\Circular; |
6
|
|
|
use AlecRabbit\Cli\Tools\Cursor; |
7
|
|
|
use AlecRabbit\Spinner\Core\Adapters\EchoOutputAdapter; |
8
|
|
|
use AlecRabbit\Spinner\Core\Contracts\Frames; |
9
|
|
|
use AlecRabbit\Spinner\Core\Contracts\SettingsInterface; |
10
|
|
|
use AlecRabbit\Spinner\Core\Contracts\SpinnerInterface; |
11
|
|
|
use AlecRabbit\Spinner\Core\Contracts\SpinnerOutputInterface; |
12
|
|
|
use function AlecRabbit\typeOf; |
13
|
|
|
use const AlecRabbit\ESC; |
14
|
|
|
|
15
|
|
|
abstract class Spinner implements SpinnerInterface |
16
|
|
|
{ |
17
|
|
|
protected const INTERVAL = SettingsInterface::DEFAULT_INTERVAL; |
18
|
|
|
protected const FRAMES = Frames::DIAMOND; |
19
|
|
|
protected const STYLES = []; |
20
|
|
|
|
21
|
|
|
/** @var string */ |
22
|
|
|
protected $messageStr; |
23
|
|
|
/** @var string */ |
24
|
|
|
protected $currentMessage; |
25
|
|
|
/** @var string */ |
26
|
|
|
protected $currentMessagePrefix; |
27
|
|
|
/** @var string */ |
28
|
|
|
protected $currentMessageSuffix; |
29
|
|
|
/** @var string */ |
30
|
|
|
protected $percentStr = ''; |
31
|
|
|
/** @var int */ |
32
|
|
|
protected $percentStrLen = 0; |
33
|
|
|
/** @var string */ |
34
|
|
|
protected $percentSpacer; |
35
|
|
|
/** @var string */ |
36
|
|
|
protected $moveBackSequenceStr; |
37
|
|
|
/** @var string */ |
38
|
|
|
protected $inlinePaddingStr; |
39
|
|
|
/** @var string */ |
40
|
|
|
protected $eraseBySpacesStr; |
41
|
|
|
/** @var Style */ |
42
|
|
|
protected $style; |
43
|
|
|
/** @var float */ |
44
|
|
|
protected $interval; |
45
|
|
|
/** @var int */ |
46
|
|
|
protected $frameErasingShift; |
47
|
|
|
/** @var Circular */ |
48
|
|
|
protected $symbols; |
49
|
|
|
/** @var null|SpinnerOutputInterface */ |
50
|
|
|
protected $output; |
51
|
|
|
/** @var int */ |
52
|
|
|
protected $messageErasingLen; |
53
|
|
|
/** @var string */ |
54
|
|
|
protected $spacer; |
55
|
|
|
/** @var SettingsInterface */ |
56
|
|
|
protected $settings; |
57
|
|
|
/** @var int */ |
58
|
|
|
protected $currentMessagePrefixLen; |
59
|
|
|
/** @var int */ |
60
|
|
|
protected $currentMessageSuffixLen; |
61
|
|
|
/** @var int */ |
62
|
|
|
protected $inlinePaddingStrLen; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* AbstractSpinner constructor. |
66
|
|
|
* |
67
|
|
|
* @param mixed $settings |
68
|
|
|
* @param null|false|SpinnerOutputInterface $output |
69
|
|
|
* @param mixed $color |
70
|
|
|
*/ |
71
|
24 |
|
public function __construct($settings = null, $output = null, $color = null) |
72
|
|
|
{ |
73
|
24 |
|
$this->output = $this->refineOutput($output); |
74
|
22 |
|
$this->settings = $this->refineSettings($settings); |
75
|
21 |
|
$this->loadSettings($color); |
76
|
20 |
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @param null|false|SpinnerOutputInterface $output |
80
|
|
|
* @return null|SpinnerOutputInterface |
81
|
|
|
*/ |
82
|
24 |
|
protected function refineOutput($output): ?SpinnerOutputInterface |
83
|
|
|
{ |
84
|
24 |
|
$this->assertOutput($output); |
85
|
22 |
|
if (false === $output) { |
86
|
13 |
|
return null; |
87
|
|
|
} |
88
|
9 |
|
return $output ?? new EchoOutputAdapter(); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param mixed $output |
93
|
|
|
*/ |
94
|
24 |
|
protected function assertOutput($output): void |
95
|
|
|
{ |
96
|
24 |
|
if (null !== $output && false !== $output && !$output instanceof SpinnerOutputInterface) { |
97
|
2 |
|
$typeOrValue = true === $output ? 'true' : typeOf($output); |
98
|
2 |
|
throw new \InvalidArgumentException( |
99
|
|
|
'Incorrect $output param' . |
100
|
|
|
' [null|false|SpinnerOutputInterface] expected' |
101
|
2 |
|
. ' "' . $typeOrValue . '" given.' |
102
|
|
|
); |
103
|
|
|
} |
104
|
22 |
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @param mixed $settings |
108
|
|
|
* @return SettingsInterface |
109
|
|
|
*/ |
110
|
22 |
|
protected function refineSettings($settings): SettingsInterface |
111
|
|
|
{ |
112
|
22 |
|
$this->assertSettings($settings); |
113
|
21 |
|
if (\is_string($settings)) { |
114
|
|
|
return |
115
|
15 |
|
$this->defaultSettings()->setMessage($settings); |
116
|
|
|
} |
117
|
|
|
return |
118
|
6 |
|
$settings ?? $this->defaultSettings(); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @param mixed $settings |
123
|
|
|
*/ |
124
|
22 |
|
protected function assertSettings($settings): void |
125
|
|
|
{ |
126
|
22 |
|
if (null !== $settings && !\is_string($settings) && !$settings instanceof SettingsInterface) { |
127
|
1 |
|
throw new \InvalidArgumentException( |
128
|
1 |
|
'Instance of SettingsInterface or string expected ' . typeOf($settings) . ' given.' |
129
|
|
|
); |
130
|
|
|
} |
131
|
21 |
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @return SettingsInterface |
135
|
|
|
*/ |
136
|
17 |
|
protected function defaultSettings(): SettingsInterface |
137
|
|
|
{ |
138
|
|
|
return |
139
|
17 |
|
(new Settings()) |
140
|
17 |
|
->setInterval(static::INTERVAL) |
141
|
17 |
|
->setSymbols(static::FRAMES) |
142
|
17 |
|
->setStyles(static::STYLES); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @param mixed $color |
147
|
|
|
*/ |
148
|
21 |
|
protected function loadSettings($color): void |
149
|
|
|
{ |
150
|
21 |
|
$this->interval = $this->settings->getInterval(); |
151
|
21 |
|
$this->frameErasingShift = $this->settings->getErasingShift(); |
152
|
21 |
|
$this->inlinePaddingStr = $this->settings->getInlinePaddingStr(); |
153
|
21 |
|
$this->currentMessage = $this->settings->getMessage(); |
154
|
21 |
|
$this->messageErasingLen = $this->settings->getMessageErasingLen(); |
155
|
21 |
|
$this->currentMessagePrefix = $this->settings->getMessagePrefix(); |
156
|
21 |
|
$this->currentMessageSuffix = $this->settings->getMessageSuffix(); |
157
|
21 |
|
$this->spacer = $this->settings->getSpacer(); |
158
|
21 |
|
$this->symbols = new Circular($this->settings->getSymbols()); |
159
|
|
|
|
160
|
|
|
try { |
161
|
21 |
|
$this->style = new Style($this->settings->getStyles(), $color); |
162
|
1 |
|
} catch (\Throwable $e) { |
163
|
1 |
|
throw new \InvalidArgumentException( |
164
|
1 |
|
'[' . static::class . '] ' . $e->getMessage(), |
165
|
1 |
|
(int)$e->getCode(), |
166
|
1 |
|
$e |
167
|
|
|
); |
168
|
|
|
} |
169
|
20 |
|
$this->inlinePaddingStrLen = strlen($this->inlinePaddingStr); // TODO fix code duplicate? |
170
|
20 |
|
$this->currentMessagePrefixLen = strlen($this->currentMessagePrefix); |
171
|
20 |
|
$this->currentMessageSuffixLen = strlen($this->currentMessageSuffix); |
172
|
20 |
|
$this->messageStr = $this->prepareMessageStr(); |
173
|
20 |
|
$this->updateProperties(); |
174
|
20 |
|
} |
175
|
|
|
|
176
|
20 |
|
protected function prepareMessageStr(): string |
177
|
|
|
{ |
178
|
|
|
return |
179
|
20 |
|
$this->spacer . |
180
|
20 |
|
$this->currentMessagePrefix . |
181
|
20 |
|
ucfirst($this->currentMessage) . |
182
|
20 |
|
$this->currentMessageSuffix; |
183
|
|
|
} |
184
|
|
|
|
185
|
20 |
|
protected function updateProperties(): void |
186
|
|
|
{ |
187
|
20 |
|
$this->percentSpacer = $this->getPercentSpacer(); // TODO move to other location - optimize performance |
188
|
|
|
$strLen = |
189
|
20 |
|
$this->currentMessagePrefixLen + |
190
|
20 |
|
$this->messageErasingLen + |
191
|
20 |
|
$this->currentMessageSuffixLen + |
192
|
20 |
|
$this->percentStrLen + |
193
|
20 |
|
$this->inlinePaddingStrLen + |
194
|
20 |
|
$this->frameErasingShift; |
195
|
20 |
|
$this->moveBackSequenceStr = ESC . "[{$strLen}D"; |
196
|
20 |
|
$this->eraseBySpacesStr = str_repeat(SettingsInterface::ONE_SPACE_SYMBOL, $strLen); |
197
|
20 |
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* @return string |
201
|
|
|
*/ |
202
|
20 |
|
protected function getPercentSpacer(): string |
203
|
|
|
{ |
204
|
20 |
|
if (strpos($this->messageStr, SettingsInterface::DEFAULT_SUFFIX)) { |
205
|
17 |
|
return SettingsInterface::ONE_SPACE_SYMBOL; |
206
|
|
|
} |
207
|
3 |
|
return SettingsInterface::EMPTY; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** {@inheritDoc} */ |
211
|
3 |
|
public function getOutput(): ?SpinnerOutputInterface |
212
|
|
|
{ |
213
|
3 |
|
return $this->output; |
214
|
|
|
} |
215
|
|
|
|
216
|
3 |
|
public function interval(): float |
217
|
|
|
{ |
218
|
3 |
|
return $this->interval; |
219
|
|
|
} |
220
|
|
|
|
221
|
4 |
|
public function inline(bool $inline): SpinnerInterface |
222
|
|
|
{ |
223
|
4 |
|
$this->inlinePaddingStr = $inline ? SettingsInterface::ONE_SPACE_SYMBOL : SettingsInterface::EMPTY; |
224
|
4 |
|
$this->inlinePaddingStrLen = strlen($this->inlinePaddingStr); |
225
|
4 |
|
$this->updateProperties(); |
226
|
4 |
|
return $this; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** {@inheritDoc} */ |
230
|
16 |
|
public function begin(?float $percent = null): string |
231
|
|
|
{ |
232
|
16 |
|
if ($this->output) { |
233
|
3 |
|
$this->output->write(Cursor::hide()); |
234
|
3 |
|
$this->spin($percent); |
235
|
3 |
|
return ''; |
236
|
|
|
} |
237
|
13 |
|
return Cursor::hide() . $this->spin($percent); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** {@inheritDoc} */ |
241
|
16 |
|
public function spin(?float $percent = null, ?string $message = null): string |
242
|
|
|
{ |
243
|
16 |
|
$this->update($percent, $message); |
244
|
16 |
|
if ($this->output) { |
245
|
3 |
|
$this->output->write($this->preparedStr()); |
246
|
3 |
|
return ''; |
247
|
|
|
} |
248
|
|
|
return |
249
|
13 |
|
$this->preparedStr(); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* @param null|float $percent |
254
|
|
|
* @param null|string $message |
255
|
|
|
*/ |
256
|
16 |
|
protected function update(?float $percent, ?string $message): void |
257
|
|
|
{ |
258
|
16 |
|
if ((null !== $percent) && 0 === ($percentVal = (int)($percent * 1000)) % 10) { |
259
|
4 |
|
$this->percentStr = $this->percentSpacer . ($percentVal / 10) . '%'; |
260
|
4 |
|
$this->percentStrLen = strlen($this->percentStr); |
261
|
|
|
} |
262
|
16 |
|
if ((null !== $message) && $this->currentMessage !== $message) { |
263
|
1 |
|
$this->currentMessage = $message; |
264
|
1 |
|
$this->messageErasingLen = strlen($message); |
265
|
1 |
|
$this->messageStr = $this->prepareMessageStr(); |
266
|
|
|
} |
267
|
16 |
|
if (null !== $percent || null !== $message) { |
268
|
5 |
|
$this->updateProperties(); |
269
|
|
|
} |
270
|
16 |
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* @return string |
274
|
|
|
*/ |
275
|
16 |
|
protected function preparedStr(): string |
276
|
|
|
{ |
277
|
|
|
return |
278
|
16 |
|
$this->inlinePaddingStr . |
279
|
16 |
|
$this->style->spinner((string)$this->symbols->value()) . |
280
|
16 |
|
$this->style->message( |
281
|
16 |
|
$this->message() |
282
|
|
|
) . |
283
|
16 |
|
$this->style->percent( |
284
|
16 |
|
$this->percent() |
285
|
|
|
) . |
286
|
16 |
|
$this->moveBackSequenceStr; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @return string |
291
|
|
|
*/ |
292
|
16 |
|
protected function message(): string |
293
|
|
|
{ |
294
|
16 |
|
return $this->messageStr; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* @return string |
299
|
|
|
*/ |
300
|
16 |
|
protected function percent(): string |
301
|
|
|
{ |
302
|
16 |
|
return $this->percentStr; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** {@inheritDoc} */ |
306
|
16 |
|
public function end(): string |
307
|
|
|
{ |
308
|
16 |
|
if ($this->output) { |
309
|
3 |
|
$this->erase(); |
310
|
3 |
|
$this->output->write(Cursor::show()); |
311
|
3 |
|
return ''; |
312
|
|
|
} |
313
|
13 |
|
return $this->erase() . Cursor::show(); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** {@inheritDoc} */ |
317
|
16 |
|
public function erase(): string |
318
|
|
|
{ |
319
|
16 |
|
$str = $this->eraseBySpacesStr . $this->moveBackSequenceStr; |
320
|
16 |
|
if ($this->output) { |
321
|
3 |
|
$this->output->write($str); |
322
|
3 |
|
return ''; |
323
|
|
|
} |
324
|
13 |
|
return $str; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** {@inheritDoc} */ |
328
|
|
|
public function getSettings(): SettingsInterface |
329
|
|
|
{ |
330
|
|
|
throw new \RuntimeException(static::class . ': Call to unimplemented functionality ' . __METHOD__); |
331
|
|
|
return $this->settings; |
|
|
|
|
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.