Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.41% covered (success)
97.41%
339 / 348
91.89% covered (success)
91.89%
68 / 74
CRAP
0.00% covered (danger)
0.00%
0 / 1
Console
97.41% covered (success)
97.41%
339 / 348
91.89% covered (success)
91.89%
68 / 74
194
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 detectTerminalSize
70.00% covered (success)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 setWrap
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMargin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setWidth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeight
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setFooter
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setHeaderSent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHelpColors
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 setInputStream
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isWindows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMargin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasInputStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getFooter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getHeaderSent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHelpColors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInputStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAvailableColors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getServer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getEnv
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addCommand
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCommands
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCommands
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasCommand
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandsFromRoutes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addCommandsFromRoutes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 help
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 line
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 resolveSize
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 buildAlignedMessageLines
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 header
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 headerLeft
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 headerCenter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 headerRight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alert
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 alertBox
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 alertDanger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertWarning
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertSuccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertInfo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertPrimary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertSecondary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertDark
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alertLight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 table
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 progressBar
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 prompt
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
10
 confirm
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 colorize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 append
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
8
 write
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 send
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 displayHelp
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 formatHelpLabel
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 colorizeHelpName
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 colorizeHelpParams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
11
 buildHelpRow
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatTemplate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getPromptInput
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 calculatePad
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Console;
16
17use Pop\Router\Match\Cli;
18use ReflectionClass;
19
20/**
21 * Console class
22 *
23 * @category   Pop
24 * @package    Pop\Console
25 * @author     Nick Sagona, III <dev@noladev.com>
26 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    5.0.0
29 */
30class Console
31{
32
33    /**
34     * Console wrap
35     * @var ?int
36     */
37    protected ?int $wrap = null;
38
39    /**
40     * Console margin
41     * @var ?int
42     */
43    protected ?int $margin = null;
44
45    /**
46     * Console terminal width
47     * @var int
48     */
49    protected int $width = 0;
50
51    /**
52     * Console terminal height
53     * @var int
54     */
55    protected int $height = 0;
56
57    /**
58     * Console response body
59     * @var ?string
60     */
61    protected ?string $response = null;
62
63    /**
64     * Command registry
65     * @var CommandRegistry
66     */
67    protected CommandRegistry $commands;
68
69    /**
70     * Console header
71     * @var ?string
72     */
73    protected ?string $header = null;
74
75    /**
76     * Flag for if console header has been sent
77     * @var bool
78     */
79    protected bool $headerSent = false;
80
81    /**
82     * Console footer
83     * @var ?string
84     */
85    protected ?string $footer = null;
86
87    /**
88     * Help colors
89     * @var array
90     */
91    protected array $helpColors = [];
92
93    /**
94     * SERVER array
95     * @var array
96     */
97    protected array $server = [];
98
99    /**
100     * ENV array
101     * @var array
102     */
103    protected array $env = [];
104
105    /**
106     * Custom input stream for prompt input (used in place of php://stdin when set)
107     * @var mixed
108     */
109    protected mixed $inputStream = null;
110
111    /**
112     * Detected terminal size, cached per-process so the stty/tput subprocess
113     * probe only ever runs once no matter how many Console instances are built
114     * @var ?array
115     */
116    protected static ?array $detectedTerminalSize = null;
117
118    /**
119     * Instantiate a new console object
120     *
121     * @param  ?int            $wrap
122     * @param  int|string|null $margin
123     */
124    public function __construct(?int $wrap = 80, int|string|null $margin = 4)
125    {
126        if (function_exists('exec') && stream_isatty(STDIN)) {
127            if (self::$detectedTerminalSize === null) {
128                self::$detectedTerminalSize = self::detectTerminalSize();
129            }
130
131            [$height, $width] = self::$detectedTerminalSize;
132            if (!empty($height) && !empty($width)) {
133                $this->setHeight((int)$height);
134                $this->setWidth((int)$width);
135            }
136        }
137
138        if ($wrap !== null) {
139            $this->setWrap($wrap);
140        }
141
142        if (is_string($margin) && str_contains($margin, ' ')) {
143            $this->setIndent($margin);
144        } else if (is_numeric($margin)) {
145            $this->setMargin((int)$margin);
146        }
147
148        $this->server   = $_SERVER;
149        $this->env      = $_ENV;
150        $this->commands = new CommandRegistry();
151    }
152
153    /**
154     * Detect the terminal height/width via stty or tput
155     *
156     * @return array
157     */
158    protected static function detectTerminalSize(): array
159    {
160        $height = null;
161        $width  = null;
162
163        if (!empty(exec('which stty'))) {
164            $sttySize = exec('stty size');
165            if (!empty($sttySize) && str_contains($sttySize, ' ')) {
166                [$height, $width] = explode(' ', $sttySize, 2);
167            }
168        } else if (!empty(exec('which tput'))) {
169            $height = exec('tput lines');
170            $width  = exec('tput cols');
171        }
172
173        return [$height, $width];
174    }
175
176    /**
177     * Set the wrap width of the console object
178     *
179     * @param  int $wrap
180     * @return Console
181     */
182    public function setWrap(int $wrap): Console
183    {
184        $this->wrap = $wrap;
185        return $this;
186    }
187
188    /**
189     * Set the margin of the console object
190     *
191     * @param  int $margin
192     * @return Console
193     */
194    public function setMargin(int $margin): Console
195    {
196        $this->margin = $margin;
197        return $this;
198    }
199
200    /**
201     * Set the margin of the console object by way of an indentation string
202     * (to maintain backwards compatibility)
203     *
204     * @param  string $indent
205     * @return Console
206     */
207    public function setIndent(string $indent): Console
208    {
209        $this->margin = strlen($indent);
210        return $this;
211    }
212
213    /**
214     * Set the terminal width of the console object
215     *
216     * @param  int $width
217     * @return Console
218     */
219    public function setWidth(int $width): Console
220    {
221        $this->width = $width;
222        return $this;
223    }
224
225    /**
226     * Set the terminal height of the console object
227     *
228     * @param  int $height
229     * @return Console
230     */
231    public function setHeight(int $height): Console
232    {
233        $this->height = $height;
234        return $this;
235    }
236
237    /**
238     * Set the console header
239     *
240     * @param  string $header
241     * @param  bool   $newline
242     * @return Console
243     */
244    public function setHeader(string $header, bool $newline = true): Console
245    {
246        $this->header = $header;
247        if ($newline) {
248            $this->header .= PHP_EOL;
249        }
250        return $this;
251    }
252
253    /**
254     * Set the console footer
255     *
256     * @param  string $footer
257     * @param  bool   $newline
258     * @return Console
259     */
260    public function setFooter(string $footer, bool $newline = true): Console
261    {
262        $this->footer = $footer;
263        if ($newline) {
264            $this->footer = PHP_EOL . $this->footer;
265        }
266        return $this;
267    }
268
269    /**
270     * Set the console header sent flag
271     *
272     * @param  bool $headerSent
273     * @return Console
274     */
275    public function setHeaderSent(bool $headerSent = true): Console
276    {
277        $this->headerSent = $headerSent;
278        return $this;
279    }
280
281    /**
282     * Set the console help colors
283     *
284     * @param  int  $color1
285     * @param  ?int $color2
286     * @param  ?int $color3
287     * @param  ?int $color4
288     * @return Console
289     */
290    public function setHelpColors(int $color1, ?int $color2 = null, ?int $color3 = null, ?int $color4 = null): Console
291    {
292        $this->helpColors = [
293            $color1
294        ];
295        if ($color2 !== null) {
296            $this->helpColors[] = $color2;
297        }
298        if ($color3 !== null) {
299            $this->helpColors[] = $color3;
300        }
301        if ($color4 !== null) {
302            $this->helpColors[] = $color4;
303        }
304
305        return $this;
306    }
307
308    /**
309     * Set the input stream for prompt input
310     *
311     * @param  mixed $stream
312     * @throws Exception
313     * @return Console
314     */
315    public function setInputStream(mixed $stream): Console
316    {
317        if (!is_resource($stream)) {
318            throw new Exception('The input stream must be a valid resource.');
319        }
320
321        $this->inputStream = $stream;
322        return $this;
323    }
324
325    /**
326     * Get the wrap width of the console object
327     *
328     * @return int
329     */
330    public function getWrap(): int
331    {
332        return $this->wrap;
333    }
334
335    /**
336     * Get the margin of the console object
337     *
338     * @return int
339     */
340    public function getMargin(): int
341    {
342        return $this->margin;
343    }
344
345    /**
346     * Get the indent string based on the margin
347     * (to maintain backwards compatibility)
348     *
349     * @return string
350     */
351    public function getIndent(): string
352    {
353        return str_repeat(' ', (int)$this->margin);
354    }
355
356    /**
357     * Get the terminal width of the console object
358     *
359     * @return int
360     */
361    public function getWidth(): int
362    {
363        return $this->width;
364    }
365
366    /**
367     * Get the terminal height of the console object
368     *
369     * @return int
370     */
371    public function getHeight(): int
372    {
373        return $this->height;
374    }
375
376    /**
377     * Check is console terminal supports color
378     *
379     * @return bool
380     */
381    public function isColor(): bool
382    {
383        return (isset($_SERVER['TERM']) && (stripos($_SERVER['TERM'], 'color') !== false));
384    }
385
386    /**
387     * Check is console terminal is in a Windows environment
388     *
389     * @return bool
390     */
391    public function isWindows(): bool
392    {
393        return (stripos(PHP_OS, 'win') !== false);
394    }
395
396    /**
397     * Has wrap
398     *
399     * @return bool
400     */
401    public function hasWrap(): bool
402    {
403        return !empty($this->wrap);
404    }
405
406    /**
407     * Has margin
408     *
409     * @return bool
410     */
411    public function hasMargin(): bool
412    {
413        return !empty($this->margin);
414    }
415
416    /**
417     * Has terminal width
418     *
419     * @return bool
420     */
421    public function hasWidth(): bool
422    {
423        return !empty($this->width);
424    }
425
426    /**
427     * Has terminal height
428     *
429     * @return bool
430     */
431    public function hasHeight(): bool
432    {
433        return !empty($this->height);
434    }
435
436    /**
437     * Has input stream
438     *
439     * @return bool
440     */
441    public function hasInputStream(): bool
442    {
443        return ($this->inputStream !== null);
444    }
445
446    /**
447     * Get the console header
448     *
449     * @param  bool $formatted
450     * @return ?string
451     */
452    public function getHeader(bool $formatted = false): ?string
453    {
454        return ($formatted) ? $this->formatTemplate($this->header) : $this->header;
455    }
456
457    /**
458     * Get the console footer
459     *
460     * @param  bool $formatted
461     * @return ?string
462     */
463    public function getFooter(bool $formatted = false): ?string
464    {
465        return ($formatted) ? $this->formatTemplate($this->footer) : $this->footer;
466    }
467
468    /**
469     * Get the console header sent flag
470     *
471     * @return bool
472     */
473    public function getHeaderSent(): bool
474    {
475        return $this->headerSent;
476    }
477
478    /**
479     * Get the console help colors
480     *
481     * @return array
482     */
483    public function getHelpColors(): array
484    {
485        return $this->helpColors;
486    }
487
488    /**
489     * Get the input stream
490     *
491     * @return mixed
492     */
493    public function getInputStream(): mixed
494    {
495        return $this->inputStream;
496    }
497
498    /**
499     * Get the console help colors
500     *
501     * @return array
502     */
503    public function getAvailableColors(): array
504    {
505        return (new ReflectionClass('Pop\Console\Color'))->getConstants();
506    }
507
508    /**
509     * Get a value from $_SERVER, or the whole array
510     *
511     * @param  ?string $key
512     * @return string|array|null
513     */
514    public function getServer(?string $key = null): string|array|null
515    {
516        if ($key === null) {
517            return $this->server;
518        } else {
519            return $this->server[$key] ?? null;
520        }
521    }
522
523    /**
524     * Get a value from $_ENV, or the whole array
525     *
526     * @param  ?string $key
527     * @return string|array|null
528     */
529    public function getEnv(?string $key = null): string|array|null
530    {
531        if ($key === null) {
532            return $this->env;
533        } else {
534            return $this->env[$key] ?? null;
535        }
536    }
537
538    /**
539     * Add a command
540     *
541     * @param  Command $command
542     * @return Console
543     */
544    public function addCommand(Command $command): Console
545    {
546        $this->commands->add($command);
547        return $this;
548    }
549
550    /**
551     * Add commands
552     *
553     * @param  array $commands
554     * @return Console
555     */
556    public function addCommands(array $commands): Console
557    {
558        $this->commands->addAll($commands);
559        return $this;
560    }
561
562    /**
563     * Get commands
564     *
565     * @return array
566     */
567    public function getCommands(): array
568    {
569        return $this->commands->all();
570    }
571
572    /**
573     * Get a command
574     *
575     * @param  string $command
576     * @return Command|null
577     */
578    public function getCommand(string $command): Command|null
579    {
580        return $this->commands->get($command);
581    }
582
583    /**
584     * Check if the console object has a command
585     *
586     * @param  string $command
587     * @return bool
588     */
589    public function hasCommand(string $command): bool
590    {
591        return $this->commands->has($command);
592    }
593
594    /**
595     * Get commands from routes
596     *
597     * @param  Cli     $routeMatch
598     * @param  ?string $scriptName
599     * @return array
600     */
601    public function getCommandsFromRoutes(Cli $routeMatch, ?string $scriptName = null): array
602    {
603        return $this->commands->fromRoutes($routeMatch, $scriptName);
604    }
605
606    /**
607     * Add commands from routes
608     *
609     * @param  Cli     $routeMatch
610     * @param  ?string $scriptName
611     * @return Console
612     */
613    public function addCommandsFromRoutes(Cli $routeMatch, ?string $scriptName = null): Console
614    {
615        $this->commands->addFromRoutes($routeMatch, $scriptName);
616        return $this;
617    }
618
619    /**
620     * Get a help
621     *
622     * @param  ?string $command
623     * @param  bool    $raw
624     * @return string|null
625     */
626    public function help(?string $command = null, bool $raw = false): string|null
627    {
628        if ($command !== null) {
629            return $this->commands->get($command)?->getHelp();
630        } else {
631            $this->displayHelp($raw);
632            return null;
633        }
634    }
635
636    /**
637     * Print a horizontal line rule out to the console
638     *
639     * @param  string $char
640     * @param  ?int   $size
641     * @param  bool   $newline
642     * @param  bool   $return
643     * @return Console|string
644     */
645    public function line(string $char = '-', ?int $size = null, bool $newline = true, bool $return = false): Console|string
646    {
647        $line = '';
648
649        if ($size === null) {
650            if (!empty($this->wrap)) {
651                $size = $this->wrap;
652            } else if (!empty($this->width)) {
653                $size = $this->width - ((int)$this->margin * 2);
654            }
655        }
656
657        $line .= $this->getIndent() . str_repeat($char, $size);
658
659        if ($newline) {
660            $line .= PHP_EOL;
661        }
662
663        if ($return) {
664            return $line;
665        } else {
666            echo $line;
667            return $this;
668        }
669    }
670
671    /**
672     * Resolve an explicit/auto/null $size against the console's wrap/width,
673     * shared by header()/alert()/alertBox() so their size-resolution precedence
674     * stays identical
675     *
676     * @param  string          $text
677     * @param  int|string|null $size
678     * @param  int             $fallbackPadding
679     * @return int|string
680     */
681    protected function resolveSize(string $text, int|string|null $size, int $fallbackPadding = 0): int|string
682    {
683        if ($size === null) {
684            if (!empty($this->wrap) && (strlen($text) > $this->wrap)) {
685                $size = $this->wrap;
686            } else if (!empty($this->width) && (strlen($text) > $this->width)) {
687                $size = $this->width - ((int)$this->margin * 2);
688            } else {
689                $size = strlen($text) + $fallbackPadding;
690            }
691        } else if ($size == 'auto') {
692            if (!empty($this->wrap)) {
693                $size = $this->wrap;
694            } else if (!empty($this->width)) {
695                $size = $this->width - ((int)$this->margin * 2);
696            }
697        }
698
699        return $size;
700    }
701
702    /**
703     * Word-wrap and align a message into padded lines of $size width, shared
704     * by alert()/alertBox()
705     *
706     * @param  string $message
707     * @param  int    $size
708     * @param  string $align
709     * @param  int    $innerPad
710     * @return array
711     */
712    protected function buildAlignedMessageLines(string $message, int $size, string $align, int $innerPad): array
713    {
714        $innerSize    = $size - ($innerPad * 2);
715        $messageLines = [];
716        $lines        = (strlen($message) > $innerSize) ?
717            explode(PHP_EOL, wordwrap($message, $innerSize, PHP_EOL)) : [$message];
718
719        foreach ($lines as $line) {
720            $pad = $this->calculatePad($line, $size, $align);
721            if ($align == 'center') {
722                $messageLines[] = str_repeat(' ', $pad) . $line . str_repeat(' ', ($size - strlen($line) - $pad));
723            } else if ($align == 'left') {
724                $messageLines[] = str_repeat(' ', $innerPad) . $line . str_repeat(' ', ($size - strlen($line) - $pad - $innerPad));
725            } else if ($align == 'right') {
726                $messageLines[] = str_repeat(' ', ($size - strlen($line) - $innerPad)) . $line . str_repeat(' ', $innerPad);
727            }
728        }
729
730        return $messageLines;
731    }
732
733    /**
734     * Print a header
735     *
736     * @param  string          $string
737     * @param  string          $char
738     * @param  int|string|null $size
739     * @param  string          $align
740     * @param  bool            $newline
741     * @param  bool            $return
742     * @return Console|string
743     */
744    public function header(
745        string $string, string $char = '-', int|string|null $size = null,
746        string $align = 'left', bool $newline = true, bool $return = false
747    ): Console|string
748    {
749        $header = '';
750        $size   = $this->resolveSize($string, $size);
751
752        if (strlen($string) > $size) {
753            $lines = explode(PHP_EOL, wordwrap($string, $size, PHP_EOL));
754            foreach ($lines as $line) {
755                if (($align != 'left') && (strlen($line) < $size)) {
756                    $line = str_repeat(' ', $this->calculatePad($line, $size, $align)) . $line;
757                }
758                $header .= $this->getIndent() . $line . PHP_EOL;
759            }
760        } else {
761            if (($align != 'left') && (strlen($string) < $size)) {
762                $string = str_repeat(' ', $this->calculatePad($string, $size, $align)) . $string;
763            }
764            $header = $this->getIndent() . $string . PHP_EOL;
765        }
766
767        if ($return) {
768            $header .= $this->line($char, $size, $newline, $return);
769            return $header;
770        } else {
771            echo $header;
772            $this->line($char, $size, $newline, $return);
773            return $this;
774        }
775    }
776
777    /**
778     * Print a left header
779     *
780     * @param  string          $string
781     * @param  string          $char
782     * @param  int|string|null $size
783     * @param  bool            $newline
784     * @param  bool            $return
785     * @return Console|string
786     */
787    public function headerLeft(
788        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
789    ): Console|string
790    {
791        return $this->header($string, $char, $size, 'left', $newline, $return);
792    }
793
794    /**
795     * Print a center header
796     *
797     * @param  string          $string
798     * @param  string          $char
799     * @param  int|string|null $size
800     * @param  bool            $newline
801     * @param  bool            $return
802     * @return Console|string
803     */
804    public function headerCenter(
805        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
806    ): Console|string
807    {
808        return $this->header($string, $char, $size, 'center', $newline, $return);
809    }
810
811    /**
812     * Print a right header
813     *
814     * @param  string          $string
815     * @param  string          $char
816     * @param  int|string|null $size
817     * @param  bool            $newline
818     * @param  bool            $return
819     * @return Console|string
820     */
821    public function headerRight(
822        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
823    ): Console|string
824    {
825        return $this->header($string, $char, $size, 'right', $newline, $return);
826    }
827
828    /**
829     * Print a colored alert box out to the console
830     *
831     * @param  string          $message
832     * @param  int             $fg
833     * @param  int             $bg
834     * @param  int|string|null $size
835     * @param  string          $align
836     * @param  int             $innerPad
837     * @param  bool            $newline
838     * @param  bool            $return
839     * @return Console|string
840     */
841    public function alert(
842        string $message, int $fg, int $bg, int|string|null $size = null, string $align = 'center',
843        int $innerPad = 4, bool $newline = true, bool $return = false
844    ): Console|string
845    {
846        $size         = $this->resolveSize($message, $size, $innerPad * 2);
847        $messageLines = $this->buildAlignedMessageLines($message, $size, $align, $innerPad);
848
849        $alert = $this->getIndent() . Color::colorize(str_repeat(' ', $size), $fg, $bg) . PHP_EOL;
850        foreach ($messageLines as $messageLine) {
851            $alert .= $this->getIndent() . Color::colorize($messageLine, $fg, $bg) . PHP_EOL;
852        }
853        $alert .= $this->getIndent() . Color::colorize(str_repeat(' ', $size), $fg, $bg) . PHP_EOL;
854        if ($newline) {
855            $alert .= PHP_EOL;
856        }
857
858        if ($return) {
859            return $alert;
860        } else {
861            echo $alert;
862            return $this;
863        }
864    }
865
866    /**
867     * Print a colorless alert outline box out to the console
868     *
869     * @param  string          $message
870     * @param  string          $h
871     * @param  ?string         $v
872     * @param  int|string|null $size
873     * @param  string          $align
874     * @param  int             $innerPad
875     * @param  bool            $newline
876     * @param  bool            $return
877     * @return Console|string
878     */
879    public function alertBox(
880        string $message, string $h = '-', ?string $v = '|', int|string|null $size = null,
881        string $align = 'center', int $innerPad = 4, bool $newline = true, bool $return = false
882    ): Console|string
883    {
884        $size         = $this->resolveSize($message, $size, $innerPad * 2);
885        $messageLines = $this->buildAlignedMessageLines($message, $size, $align, $innerPad);
886
887        $alert  = $this->getIndent() . str_repeat($h, $size) . PHP_EOL;
888        $alert .= $this->getIndent() . $v . str_repeat(' ', $size - 2) . $v . PHP_EOL;
889        foreach ($messageLines as $messageLine) {
890            if (!empty($v) && str_starts_with($messageLine, ' ') && str_ends_with($messageLine, ' ')) {
891                $messageLine = $v . substr($messageLine, 1, -1) . $v;
892            }
893            $alert .= $this->getIndent() . $messageLine . PHP_EOL;
894        }
895        $alert .= $this->getIndent() . $v . str_repeat(' ', $size - 2) . $v . PHP_EOL;
896        $alert .= $this->getIndent() . str_repeat($h, $size) . PHP_EOL;
897        if ($newline) {
898            $alert .= PHP_EOL;
899        }
900
901        if ($return) {
902            return $alert;
903        } else {
904            echo $alert;
905            return $this;
906        }
907    }
908
909    /**
910     * Print a "danger" alert box out to the console
911     *
912     * @param  string          $message
913     * @param  int|string|null $size
914     * @param  string          $align
915     * @param  int             $innerPad
916     * @param  bool            $newline
917     * @param  bool            $return
918     * @return Console|string
919     */
920    public function alertDanger(
921        string $message, int|string|null $size = null, string $align = 'center',
922        int $innerPad = 4, bool $newline = true, bool $return = false
923    ): Console|string
924    {
925        return $this->alert($message, Color::BRIGHT_BOLD_WHITE, Color::BRIGHT_RED, $size, $align, $innerPad, $newline, $return);
926    }
927
928    /**
929     * Print a "warning" alert box out to the console
930     *
931     * @param  string          $message
932     * @param  int|string|null $size
933     * @param  string          $align
934     * @param  int             $innerPad
935     * @param  bool            $newline
936     * @param  bool            $return
937     * @return Console|string
938     */
939    public function alertWarning(
940        string $message, int|string|null $size = null, string $align = 'center',
941        int $innerPad = 4, bool $newline = true, bool $return = false
942    ): Console|string
943    {
944        return $this->alert($message, Color::BOLD_BLACK, Color::BRIGHT_YELLOW, $size, $align, $innerPad, $newline, $return);
945    }
946
947    /**
948     * Print a "success" alert box out to the console
949     *
950     * @param  string          $message
951     * @param  int|string|null $size
952     * @param  string          $align
953     * @param  int             $innerPad
954     * @param  bool            $newline
955     * @param  bool            $return
956     * @return Console|string
957     */
958    public function alertSuccess(
959        string $message, int|string|null $size = null, string $align = 'center',
960        int $innerPad = 4, bool $newline = true, bool $return = false
961    ): Console|string
962    {
963        return $this->alert($message, Color::BOLD_BLACK, Color::GREEN, $size, $align, $innerPad, $newline, $return);
964    }
965
966    /**
967     * Print an "info" alert box out to the console
968     *
969     * @param  string          $message
970     * @param  int|string|null $size
971     * @param  string          $align
972     * @param  int             $innerPad
973     * @param  bool            $newline
974     * @param  bool            $return
975     * @return Console|string
976     */
977    public function alertInfo(
978        string $message, int|string|null $size = null, string $align = 'center',
979        int $innerPad = 4, bool $newline = true, bool $return = false
980    ): Console|string
981    {
982        return $this->alert($message, Color::BRIGHT_BOLD_WHITE, Color::BRIGHT_BLUE, $size, $align, $innerPad, $newline, $return);
983    }
984
985    /**
986     * Print a "primary" alert box out to the console
987     *
988     * @param  string          $message
989     * @param  int|string|null $size
990     * @param  string          $align
991     * @param  int             $innerPad
992     * @param  bool            $newline
993     * @param  bool            $return
994     * @return Console|string
995     */
996    public function alertPrimary(
997        string $message, int|string|null $size = null, string $align = 'center',
998        int $innerPad = 4, bool $newline = true, bool $return = false
999    ): Console|string
1000    {
1001        return $this->alert($message, Color::BRIGHT_BOLD_WHITE, Color::BLUE, $size, $align, $innerPad, $newline, $return);
1002    }
1003
1004    /**
1005     * Print a "secondary" alert box out to the console
1006     *
1007     * @param  string          $message
1008     * @param  int|string|null $size
1009     * @param  string          $align
1010     * @param  int             $innerPad
1011     * @param  bool            $newline
1012     * @param  bool            $return
1013     * @return Console|string
1014     */
1015    public function alertSecondary(
1016        string $message, int|string|null $size = null, string $align = 'center',
1017        int $innerPad = 4, bool $newline = true, bool $return = false
1018    ): Console|string
1019    {
1020        return $this->alert($message, Color::BRIGHT_BOLD_WHITE, Color::MAGENTA, $size, $align, $innerPad, $newline, $return);
1021    }
1022
1023    /**
1024     * Print a "dark" alert box out to the console
1025     *
1026     * @param  string          $message
1027     * @param  int|string|null $size
1028     * @param  string          $align
1029     * @param  int             $innerPad
1030     * @param  bool            $newline
1031     * @param  bool            $return
1032     * @return Console|string
1033     */
1034    public function alertDark(
1035        string $message, int|string|null $size = null, string $align = 'center',
1036        int $innerPad = 4, bool $newline = true, bool $return = false
1037    ): Console|string
1038    {
1039        return $this->alert($message, Color::BRIGHT_BOLD_WHITE, Color::BRIGHT_BLACK, $size, $align, $innerPad, $newline, $return);
1040    }
1041
1042    /**
1043     * Print a "light" alert box out to the console
1044     *
1045     * @param  string          $message
1046     * @param  int|string|null $size
1047     * @param  string          $align
1048     * @param  int             $innerPad
1049     * @param  bool            $newline
1050     * @param  bool            $return
1051     * @return Console|string
1052     */
1053    public function alertLight(
1054        string $message, int|string|null $size = null, string $align = 'center',
1055        int $innerPad = 4, bool $newline = true, bool $return = false
1056    ): Console|string
1057    {
1058        return $this->alert($message, Color::BOLD_BLACK, Color::WHITE, $size, $align, $innerPad, $newline, $return);
1059    }
1060
1061    /**
1062     * Print a table out to the console
1063     *
1064     * @param  array   $headers
1065     * @param  array   $rows
1066     * @param  string  $h
1067     * @param  ?string $v
1068     * @param  ?int    $headerFg
1069     * @param  ?int    $headerBg
1070     * @param  bool    $newline
1071     * @param  bool    $return
1072     * @return Console|string
1073     */
1074    public function table(
1075        array $headers, array $rows, string $h = '-', ?string $v = '|',
1076        ?int $headerFg = null, ?int $headerBg = null, bool $newline = true, bool $return = false
1077    ): Console|string
1078    {
1079        $table = new Table($headers, $rows, $h, $v);
1080        if (($headerFg !== null) || ($headerBg !== null)) {
1081            $table->setHeaderColor($headerFg, $headerBg);
1082        }
1083
1084        $output = '';
1085        foreach (explode(PHP_EOL, rtrim($table->render(), PHP_EOL)) as $line) {
1086            $output .= $this->getIndent() . $line . PHP_EOL;
1087        }
1088        if ($newline) {
1089            $output .= PHP_EOL;
1090        }
1091
1092        if ($return) {
1093            return $output;
1094        } else {
1095            echo $output;
1096            return $this;
1097        }
1098    }
1099
1100    /**
1101     * Create a progress bar
1102     *
1103     * @param  int     $total
1104     * @param  ?string $message
1105     * @param  int     $width
1106     * @return ProgressBar
1107     */
1108    public function progressBar(int $total, ?string $message = null, int $width = 28): ProgressBar
1109    {
1110        $bar = new ProgressBar($total, $message, $width);
1111        $bar->setIndent($this->getIndent());
1112        return $bar;
1113    }
1114
1115    /**
1116     * Get input from the prompt
1117     *
1118     * @param  string $prompt
1119     * @param  ?array $options
1120     * @param  bool   $caseSensitive
1121     * @param  int    $length
1122     * @param  bool   $withHeaders
1123     * @return string
1124     */
1125    public function prompt(
1126        string $prompt, ?array $options = null, bool $caseSensitive = false, int $length = 500, bool $withHeaders = true
1127    ): string
1128    {
1129        if (($withHeaders) && ($this->header !== null)) {
1130            $this->headerSent = true;
1131            echo $this->formatTemplate($this->header) . $this->getIndent() . $prompt;
1132        } else {
1133            echo $this->getIndent() . $prompt;
1134        }
1135
1136        $input = null;
1137
1138        if ($options !== null) {
1139            $length = 0;
1140            foreach ($options as $key => $value) {
1141                $options[$key] = ($caseSensitive) ? $value : strtolower((string)$value);
1142                if (strlen((string)$value) > $length) {
1143                    $length = strlen((string)$value);
1144                }
1145            }
1146
1147            while (!in_array($input, $options)) {
1148                if ($input !== null) {
1149                    echo $this->getIndent() . $prompt;
1150                }
1151                $input = $this->getPromptInput($prompt, $length, $caseSensitive);
1152            }
1153        } else {
1154            while ($input === null) {
1155                $input = $this->getPromptInput($prompt, $length, $caseSensitive);
1156            }
1157        }
1158
1159        return $input;
1160    }
1161
1162    /**
1163     * Display confirm message prompt
1164     *
1165     * @param  string $message
1166     * @param  array  $options
1167     * @param  bool   $caseSensitive
1168     * @param  int    $length
1169     * @param  bool   $withHeaders
1170     * @param  bool   $exit
1171     * @return string
1172     */
1173    public function confirm(
1174        string $message = 'Are you sure?', array $options = ['Y', 'N'], bool $caseSensitive = false,
1175        int $length = 500, bool $withHeaders = true, bool $exit = true
1176    ): string
1177    {
1178        $message .= ' [' . implode('/', $options) . '] ';
1179        $response = $this->prompt($message, $options, $caseSensitive, $length, $withHeaders);
1180
1181        if (($exit) && ((strtolower($response) == 'n') || (strtolower($response) == 'no'))) {
1182            echo PHP_EOL;
1183            exit(127);
1184        }
1185
1186        return $response;
1187    }
1188
1189    /**
1190     * Colorize a string for output
1191     *
1192     * @param  string $string
1193     * @param  ?int   $fg
1194     * @param  ?int   $bg
1195     * @return string
1196     */
1197    public function colorize(string $string, ?int $fg = null, ?int $bg = null): string
1198    {
1199        return Color::colorize($string, $fg, $bg);
1200    }
1201
1202    /**
1203     * Append a string of text to the response body
1204     *
1205     * @param  ?string $text
1206     * @param  bool    $newline
1207     * @param  bool    $margin
1208     * @return Console
1209     */
1210    public function append(?string $text = null, bool $newline = true, bool $margin = true): Console
1211    {
1212        if (!empty($this->wrap)) {
1213            $lines = (strlen((string)$text) > $this->wrap) ?
1214                explode(PHP_EOL, wordwrap($text, $this->wrap, PHP_EOL)) : [$text];
1215        } else if (!empty($this->width)) {
1216            $lines = (strlen((string)$text) > ($this->width - ((int)$this->margin * 2))) ?
1217                explode(PHP_EOL, wordwrap($text, ($this->width - ((int)$this->margin * 2)), PHP_EOL)) : [$text];
1218        } else {
1219            $lines = [$text];
1220        }
1221
1222        foreach ($lines as $line) {
1223            $this->response .= (($margin) ? $this->getIndent() : '') . $line . (($newline) ? PHP_EOL : null);
1224        }
1225
1226        return $this;
1227    }
1228
1229    /**
1230     * Write a string of text to the response body and send the response
1231     *
1232     * @param  ?string $text
1233     * @param  bool    $newline
1234     * @param  bool    $margin
1235     * @param  bool    $withHeaders
1236     * @return Console
1237     */
1238    public function write(?string $text = null, bool $newline = true, bool $margin = true, bool $withHeaders = true): Console
1239    {
1240        $this->append($text, $newline, $margin);
1241        $this->send($withHeaders);
1242        return $this;
1243    }
1244
1245    /**
1246     * Send the response
1247     *
1248     * @param  bool $withHeaders
1249     * @return Console
1250     */
1251    public function send(bool $withHeaders = true): Console
1252    {
1253        if ($withHeaders) {
1254            if (($this->header !== null) && !($this->headerSent)) {
1255                $this->response = $this->formatTemplate($this->header) . $this->response;
1256            }
1257            if ($this->footer !== null) {
1258                $this->response .= $this->formatTemplate($this->footer);
1259            }
1260        }
1261
1262        echo $this->response;
1263        $this->response = null;
1264        return $this;
1265    }
1266
1267    /**
1268     * Display console help
1269     *
1270     * @return void
1271     */
1272    public function displayHelp(bool $raw = false): void
1273    {
1274        $this->response = null;
1275        $registry       = $this->commands->all();
1276        $commands       = [];
1277        $commandLengths = [];
1278
1279        if ($this->header !== null) {
1280            $this->response .= $this->formatTemplate($this->header);
1281        }
1282
1283        foreach ($registry as $key => $command) {
1284            [$label, $length]     = $this->formatHelpLabel($command, $raw);
1285            $commands[$key]       = $this->getIndent() . $label;
1286            $commandLengths[$key] = $length;
1287        }
1288
1289        $maxLength = max($commandLengths);
1290        $wrapped   = false;
1291        $total     = count($commands);
1292        $i         = 0;
1293
1294        foreach ($commands as $key => $command) {
1295            [$row, $wrapped] = $this->buildHelpRow(
1296                $registry[$key], $command, $commandLengths[$key], $maxLength, ($i == $total - 1), $wrapped
1297            );
1298            $this->response .= $row;
1299            $i++;
1300        }
1301
1302        if ($this->footer !== null) {
1303            $this->response .= $this->formatTemplate($this->footer);
1304        }
1305
1306        $this->send(false);
1307    }
1308
1309    /**
1310     * Format a registered command's name/params into a colorized help label
1311     *
1312     * @param  Command\CommandInterface $command
1313     * @param  bool                     $raw
1314     * @return array
1315     */
1316    protected function formatHelpLabel(Command\CommandInterface $command, bool $raw): array
1317    {
1318        $name   = $command->getName();
1319        $params = $command->getParams();
1320        $length = strlen((string)$name);
1321
1322        if (count($this->helpColors) > 0) {
1323            $name = $this->colorizeHelpName((string)$name, $raw);
1324        }
1325
1326        if ($params !== null) {
1327            $length += (strlen((string)$params) + 1);
1328            $name   .= $this->colorizeHelpParams($params, $raw);
1329        }
1330
1331        return [$name, $length];
1332    }
1333
1334    /**
1335     * Colorize a command name (and its sub-name, if space-separated) for the help screen
1336     *
1337     * @param  string $name
1338     * @param  bool   $raw
1339     * @return string
1340     */
1341    protected function colorizeHelpName(string $name, bool $raw): string
1342    {
1343        if (str_contains($name, ' ')) {
1344            $name1 = substr($name, 0, strpos($name, ' '));
1345            $name2 = substr($name, strpos($name, ' ') + 1);
1346            if (isset($this->helpColors[0])) {
1347                $name1 = Color::colorize($name1, $this->helpColors[0], null, $raw);
1348            }
1349            if (isset($this->helpColors[1])) {
1350                $name2 = Color::colorize($name2, $this->helpColors[1], null, $raw);
1351            }
1352            return $name1 . ' ' . $name2;
1353        } else if (isset($this->helpColors[0])) {
1354            return Color::colorize($name, $this->helpColors[0], null, $raw);
1355        }
1356
1357        return $name;
1358    }
1359
1360    /**
1361     * Colorize a command's params for the help screen
1362     *
1363     * @param  string $params
1364     * @param  bool   $raw
1365     * @return string
1366     */
1367    protected function colorizeHelpParams(string $params, bool $raw): string
1368    {
1369        if (str_contains($params, '-') && str_contains($params, '<')) {
1370            $pars        = explode(' ', $params);
1371            $optionFirst = str_contains($pars[0], '-');
1372            $colorIndex  = 2;
1373            $colored     = '';
1374            foreach ($pars as $p) {
1375                if (isset($this->helpColors[3]) &&
1376                    (($optionFirst) && str_contains($p, '<')) || ((!$optionFirst) && str_contains($p, '-'))) {
1377                    $colorIndex = 3;
1378                }
1379                $colored .= ' ' . ((isset($this->helpColors[$colorIndex])) ?
1380                        Color::colorize($p, $this->helpColors[$colorIndex], null, $raw) : $p);
1381            }
1382            return $colored;
1383        }
1384
1385        return ' ' . ((isset($this->helpColors[2])) ?
1386                Color::colorize($params, $this->helpColors[2], null, $raw) : $params);
1387    }
1388
1389    /**
1390     * Build one command's row for the help screen, wrapping its help text if needed
1391     *
1392     * @param  Command\CommandInterface $command
1393     * @param  string                   $label
1394     * @param  int                      $length
1395     * @param  int                      $maxLength
1396     * @param  bool                     $isLast
1397     * @param  bool                     $wrapped
1398     * @return array
1399     */
1400    protected function buildHelpRow(
1401        Command\CommandInterface $command, string $label, int $length, int $maxLength, bool $isLast, bool $wrapped
1402    ): array
1403    {
1404        if (!$command->hasHelp()) {
1405            return [$label . $command->getHelp() . PHP_EOL, $wrapped];
1406        }
1407
1408        $help = $command->getHelp();
1409        $pad  = ($length < $maxLength) ?
1410            str_repeat(' ', $maxLength - $length) . '    ' : '    ';
1411
1412        if (strlen((string)$command . $pad . $help) <= $this->wrap) {
1413            return [$label . $pad . $help . PHP_EOL, false];
1414        }
1415
1416        $row    = ($wrapped) ? '' : PHP_EOL;
1417        $offset = $this->wrap - strlen((string)$command . $pad);
1418        $lines  = explode(PHP_EOL, wordwrap($help, $offset, PHP_EOL));
1419        foreach ($lines as $lineIndex => $line) {
1420            $row .= ($lineIndex == 0) ?
1421                $label . $pad . $line . PHP_EOL :
1422                $this->getIndent() . str_repeat(' ', strlen((string)$command)) . $pad . $line . PHP_EOL;
1423        }
1424
1425        if (!$isLast) {
1426            $row .= PHP_EOL;
1427        }
1428
1429        return [$row, true];
1430    }
1431
1432    /**
1433     * Clear the console
1434     *
1435     * @return void
1436     */
1437    public function clear(): void
1438    {
1439        echo chr(27) . "[2J" . chr(27) . "[;H";
1440    }
1441
1442    /**
1443     * Format header or footer template
1444     *
1445     * @param  string $template
1446     * @return string
1447     */
1448    protected function formatTemplate(string $template): string
1449    {
1450        $format = null;
1451
1452        if (str_contains($template, "\n")) {
1453            $templateLines = explode("\n", $template);
1454            foreach ($templateLines as $line) {
1455                $line    = trim($line);
1456                $format .= $this->getIndent() . $line . PHP_EOL;
1457            }
1458        } else {
1459            $format = $this->getIndent() . $template . PHP_EOL;
1460        }
1461
1462        return $format;
1463    }
1464
1465    /**
1466     * Get prompt input
1467     *
1468     * @param  string $prompt
1469     * @param  int    $length
1470     * @param  bool   $caseSensitive
1471     * @return string
1472     */
1473    protected function getPromptInput(string $prompt, int $length = 500, bool $caseSensitive = false): string
1474    {
1475        $stream = $this->inputStream ?? fopen('php://stdin', 'r');
1476        $input  = fgets($stream, strlen((string)$prompt) + $length);
1477        $input  = ($caseSensitive) ? rtrim((string)$input) : strtolower(rtrim((string)$input));
1478
1479        if ($this->inputStream === null) {
1480            fclose($stream);
1481        }
1482
1483        return $input;
1484    }
1485
1486    /**
1487     * Calculate string pad
1488     *
1489     * @param  string $string
1490     * @param  int    $size
1491     * @param  string $align
1492     * @return int
1493     */
1494    protected function calculatePad(string $string, int $size, string $align = 'center'): int
1495    {
1496        $pad = 0;
1497
1498        if ($align == 'center') {
1499            $pad = (int)round(($size - strlen($string)) / 2);
1500        } else if ($align == 'right') {
1501            $pad = $size - strlen($string);
1502        }
1503
1504        return $pad;
1505    }
1506
1507}