Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.06% covered (success)
98.06%
253 / 258
97.01% covered (success)
97.01%
65 / 67
CRAP
0.00% covered (danger)
0.00%
0 / 1
Console
98.06% covered (success)
98.06%
253 / 258
97.01% covered (success)
97.01%
65 / 67
134
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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 header
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 headerLeft
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 headerCenter
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 headerRight
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alert
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertBox
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertDanger
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertWarning
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertSuccess
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertInfo
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertPrimary
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertSecondary
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertDark
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 alertLight
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 table
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 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%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 promptMulti
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 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%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 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
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 <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
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 <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
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     * @param  ?string $subCommand
625     * @return string|null
626     */
627    public function help(?string $command = null, bool $raw = false, ?string $subCommand = null): string|null
628    {
629        if ($command !== null) {
630            return $this->commands->get($command)?->getHelp();
631        } else {
632            $this->displayHelp($raw, $subCommand);
633            return null;
634        }
635    }
636
637    /**
638     * Print a horizontal line rule out to the console
639     *
640     * @param  string $char
641     * @param  ?int   $size
642     * @param  bool   $newline
643     * @param  bool   $return
644     * @return Console|string
645     */
646    public function line(string $char = '-', ?int $size = null, bool $newline = true, bool $return = false): Console|string
647    {
648        $result = (new Header($this->getIndent(), $this->wrap, $this->width, $this->margin))->line($char, $size, $newline);
649
650        if ($return) {
651            return $result;
652        }
653
654        echo $result;
655        return $this;
656    }
657
658    /**
659     * Print a header
660     *
661     * @param  string          $string
662     * @param  string          $char
663     * @param  int|string|null $size
664     * @param  string          $align
665     * @param  bool            $newline
666     * @param  bool            $return
667     * @return Console|string
668     */
669    public function header(
670        string $string, string $char = '-', int|string|null $size = null,
671        string $align = 'left', bool $newline = true, bool $return = false
672    ): Console|string
673    {
674        $result = (new Header($this->getIndent(), $this->wrap, $this->width, $this->margin))
675            ->header($string, $char, $size, $align, $newline);
676
677        if ($return) {
678            return $result;
679        }
680
681        echo $result;
682        return $this;
683    }
684
685    /**
686     * Print a left header
687     *
688     * @param  string          $string
689     * @param  string          $char
690     * @param  int|string|null $size
691     * @param  bool            $newline
692     * @param  bool            $return
693     * @return Console|string
694     */
695    public function headerLeft(
696        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
697    ): Console|string
698    {
699        $result = (new Header($this->getIndent(), $this->wrap, $this->width, $this->margin))
700            ->headerLeft($string, $char, $size, $newline);
701
702        if ($return) {
703            return $result;
704        }
705
706        echo $result;
707        return $this;
708    }
709
710    /**
711     * Print a center header
712     *
713     * @param  string          $string
714     * @param  string          $char
715     * @param  int|string|null $size
716     * @param  bool            $newline
717     * @param  bool            $return
718     * @return Console|string
719     */
720    public function headerCenter(
721        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
722    ): Console|string
723    {
724        $result = (new Header($this->getIndent(), $this->wrap, $this->width, $this->margin))
725            ->headerCenter($string, $char, $size, $newline);
726
727        if ($return) {
728            return $result;
729        }
730
731        echo $result;
732        return $this;
733    }
734
735    /**
736     * Print a right header
737     *
738     * @param  string          $string
739     * @param  string          $char
740     * @param  int|string|null $size
741     * @param  bool            $newline
742     * @param  bool            $return
743     * @return Console|string
744     */
745    public function headerRight(
746        string $string, string $char = '-', int|string|null $size = 'auto', bool $newline = true, bool $return = false
747    ): Console|string
748    {
749        $result = (new Header($this->getIndent(), $this->wrap, $this->width, $this->margin))
750            ->headerRight($string, $char, $size, $newline);
751
752        if ($return) {
753            return $result;
754        }
755
756        echo $result;
757        return $this;
758    }
759
760    /**
761     * Print a colored alert box out to the console
762     *
763     * @param  string          $message
764     * @param  int             $fg
765     * @param  int             $bg
766     * @param  int|string|null $size
767     * @param  string          $align
768     * @param  int             $innerPad
769     * @param  bool            $newline
770     * @param  bool            $return
771     * @return Console|string
772     */
773    public function alert(
774        string $message, int $fg, int $bg, int|string|null $size = null, string $align = 'center',
775        int $innerPad = 4, bool $newline = true, bool $return = false
776    ): Console|string
777    {
778        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
779            ->alert($message, $fg, $bg, $size, $align, $innerPad, $newline);
780
781        if ($return) {
782            return $result;
783        }
784
785        echo $result;
786        return $this;
787    }
788
789    /**
790     * Print a colorless alert outline box out to the console
791     *
792     * @param  string          $message
793     * @param  string          $h
794     * @param  ?string         $v
795     * @param  int|string|null $size
796     * @param  string          $align
797     * @param  int             $innerPad
798     * @param  bool            $newline
799     * @param  bool            $return
800     * @return Console|string
801     */
802    public function alertBox(
803        string $message, string $h = '-', ?string $v = '|', int|string|null $size = null,
804        string $align = 'center', int $innerPad = 4, bool $newline = true, bool $return = false
805    ): Console|string
806    {
807        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
808            ->alertBox($message, $h, $v, $size, $align, $innerPad, $newline);
809
810        if ($return) {
811            return $result;
812        }
813
814        echo $result;
815        return $this;
816    }
817
818    /**
819     * Print a "danger" alert box out to the console
820     *
821     * @param  string          $message
822     * @param  int|string|null $size
823     * @param  string          $align
824     * @param  int             $innerPad
825     * @param  bool            $newline
826     * @param  bool            $return
827     * @return Console|string
828     */
829    public function alertDanger(
830        string $message, int|string|null $size = null, string $align = 'center',
831        int $innerPad = 4, bool $newline = true, bool $return = false
832    ): Console|string
833    {
834        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
835            ->alertDanger($message, $size, $align, $innerPad, $newline);
836
837        if ($return) {
838            return $result;
839        }
840
841        echo $result;
842        return $this;
843    }
844
845    /**
846     * Print a "warning" alert box out to the console
847     *
848     * @param  string          $message
849     * @param  int|string|null $size
850     * @param  string          $align
851     * @param  int             $innerPad
852     * @param  bool            $newline
853     * @param  bool            $return
854     * @return Console|string
855     */
856    public function alertWarning(
857        string $message, int|string|null $size = null, string $align = 'center',
858        int $innerPad = 4, bool $newline = true, bool $return = false
859    ): Console|string
860    {
861        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
862            ->alertWarning($message, $size, $align, $innerPad, $newline);
863
864        if ($return) {
865            return $result;
866        }
867
868        echo $result;
869        return $this;
870    }
871
872    /**
873     * Print a "success" alert box out to the console
874     *
875     * @param  string          $message
876     * @param  int|string|null $size
877     * @param  string          $align
878     * @param  int             $innerPad
879     * @param  bool            $newline
880     * @param  bool            $return
881     * @return Console|string
882     */
883    public function alertSuccess(
884        string $message, int|string|null $size = null, string $align = 'center',
885        int $innerPad = 4, bool $newline = true, bool $return = false
886    ): Console|string
887    {
888        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
889            ->alertSuccess($message, $size, $align, $innerPad, $newline);
890
891        if ($return) {
892            return $result;
893        }
894
895        echo $result;
896        return $this;
897    }
898
899    /**
900     * Print an "info" alert box out to the console
901     *
902     * @param  string          $message
903     * @param  int|string|null $size
904     * @param  string          $align
905     * @param  int             $innerPad
906     * @param  bool            $newline
907     * @param  bool            $return
908     * @return Console|string
909     */
910    public function alertInfo(
911        string $message, int|string|null $size = null, string $align = 'center',
912        int $innerPad = 4, bool $newline = true, bool $return = false
913    ): Console|string
914    {
915        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
916            ->alertInfo($message, $size, $align, $innerPad, $newline);
917
918        if ($return) {
919            return $result;
920        }
921
922        echo $result;
923        return $this;
924    }
925
926    /**
927     * Print a "primary" alert box out to the console
928     *
929     * @param  string          $message
930     * @param  int|string|null $size
931     * @param  string          $align
932     * @param  int             $innerPad
933     * @param  bool            $newline
934     * @param  bool            $return
935     * @return Console|string
936     */
937    public function alertPrimary(
938        string $message, int|string|null $size = null, string $align = 'center',
939        int $innerPad = 4, bool $newline = true, bool $return = false
940    ): Console|string
941    {
942        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
943            ->alertPrimary($message, $size, $align, $innerPad, $newline);
944
945        if ($return) {
946            return $result;
947        }
948
949        echo $result;
950        return $this;
951    }
952
953    /**
954     * Print a "secondary" alert box out to the console
955     *
956     * @param  string          $message
957     * @param  int|string|null $size
958     * @param  string          $align
959     * @param  int             $innerPad
960     * @param  bool            $newline
961     * @param  bool            $return
962     * @return Console|string
963     */
964    public function alertSecondary(
965        string $message, int|string|null $size = null, string $align = 'center',
966        int $innerPad = 4, bool $newline = true, bool $return = false
967    ): Console|string
968    {
969        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
970            ->alertSecondary($message, $size, $align, $innerPad, $newline);
971
972        if ($return) {
973            return $result;
974        }
975
976        echo $result;
977        return $this;
978    }
979
980    /**
981     * Print a "dark" alert box out to the console
982     *
983     * @param  string          $message
984     * @param  int|string|null $size
985     * @param  string          $align
986     * @param  int             $innerPad
987     * @param  bool            $newline
988     * @param  bool            $return
989     * @return Console|string
990     */
991    public function alertDark(
992        string $message, int|string|null $size = null, string $align = 'center',
993        int $innerPad = 4, bool $newline = true, bool $return = false
994    ): Console|string
995    {
996        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
997            ->alertDark($message, $size, $align, $innerPad, $newline);
998
999        if ($return) {
1000            return $result;
1001        }
1002
1003        echo $result;
1004        return $this;
1005    }
1006
1007    /**
1008     * Print a "light" alert box out to the console
1009     *
1010     * @param  string          $message
1011     * @param  int|string|null $size
1012     * @param  string          $align
1013     * @param  int             $innerPad
1014     * @param  bool            $newline
1015     * @param  bool            $return
1016     * @return Console|string
1017     */
1018    public function alertLight(
1019        string $message, int|string|null $size = null, string $align = 'center',
1020        int $innerPad = 4, bool $newline = true, bool $return = false
1021    ): Console|string
1022    {
1023        $result = (new Alert($this->getIndent(), $this->wrap, $this->width, $this->margin))
1024            ->alertLight($message, $size, $align, $innerPad, $newline);
1025
1026        if ($return) {
1027            return $result;
1028        }
1029
1030        echo $result;
1031        return $this;
1032    }
1033
1034    /**
1035     * Print a table out to the console
1036     *
1037     * @param  array   $headers
1038     * @param  array   $rows
1039     * @param  string  $h
1040     * @param  ?string $v
1041     * @param  ?int    $headerFg
1042     * @param  ?int    $headerBg
1043     * @param  bool    $newline
1044     * @param  bool    $return
1045     * @return Console|string
1046     */
1047    public function table(
1048        array $headers, array $rows, string $h = '-', ?string $v = '|',
1049        ?int $headerFg = null, ?int $headerBg = null, bool $newline = true, bool $return = false
1050    ): Console|string
1051    {
1052        $table = new Table($headers, $rows, $h, $v);
1053        if (($headerFg !== null) || ($headerBg !== null)) {
1054            $table->setHeaderColor($headerFg, $headerBg);
1055        }
1056
1057        $output = '';
1058        foreach (explode(PHP_EOL, rtrim($table->render(), PHP_EOL)) as $line) {
1059            $output .= $this->getIndent() . $line . PHP_EOL;
1060        }
1061        if ($newline) {
1062            $output .= PHP_EOL;
1063        }
1064
1065        if ($return) {
1066            return $output;
1067        } else {
1068            echo $output;
1069            return $this;
1070        }
1071    }
1072
1073    /**
1074     * Create a progress bar
1075     *
1076     * @param  int     $total
1077     * @param  ?string $message
1078     * @param  int     $width
1079     * @return ProgressBar
1080     */
1081    public function progressBar(int $total, ?string $message = null, int $width = 28): ProgressBar
1082    {
1083        $bar = new ProgressBar($total, $message, $width);
1084        $bar->setIndent($this->getIndent());
1085        return $bar;
1086    }
1087
1088    /**
1089     * Get input from the prompt
1090     *
1091     * @param  string $prompt
1092     * @param  ?array $options
1093     * @param  bool   $caseSensitive
1094     * @param  int    $length
1095     * @param  bool   $withHeaders
1096     * @return string
1097     */
1098    public function prompt(
1099        string $prompt, ?array $options = null, bool $caseSensitive = false, int $length = 500, bool $withHeaders = true
1100    ): string
1101    {
1102        $formattedHeader = null;
1103        if (($withHeaders) && ($this->header !== null)) {
1104            $this->headerSent = true;
1105            $formattedHeader = $this->formatTemplate($this->header);
1106        }
1107
1108        return (new Prompt($this->getIndent(), $formattedHeader, $this->inputStream))
1109            ->prompt($prompt, $options, $caseSensitive, $length);
1110    }
1111
1112    /**
1113     * Display a prompt that accepts one or more comma-separated selections
1114     *
1115     * @param  string $prompt
1116     * @param  array  $options
1117     * @param  bool   $caseSensitive
1118     * @param  int    $length
1119     * @param  bool   $withHeaders
1120     * @return array
1121     */
1122    public function promptMulti(
1123        string $prompt, array $options, bool $caseSensitive = false, int $length = 500, bool $withHeaders = true
1124    ): array
1125    {
1126        $formattedHeader = null;
1127        if (($withHeaders) && ($this->header !== null)) {
1128            $this->headerSent = true;
1129            $formattedHeader = $this->formatTemplate($this->header);
1130        }
1131
1132        return (new Prompt($this->getIndent(), $formattedHeader, $this->inputStream))
1133            ->promptMulti($prompt, $options, $caseSensitive, $length);
1134    }
1135
1136    /**
1137     * Display confirm message prompt
1138     *
1139     * @param  string $message
1140     * @param  array  $options
1141     * @param  bool   $caseSensitive
1142     * @param  int    $length
1143     * @param  bool   $withHeaders
1144     * @param  bool   $exit
1145     * @return string
1146     */
1147    public function confirm(
1148        string $message = 'Are you sure?', array $options = ['Y', 'N'], bool $caseSensitive = false,
1149        int $length = 500, bool $withHeaders = true, bool $exit = true
1150    ): string
1151    {
1152        $message .= ' [' . implode('/', $options) . '] ';
1153        $response = $this->prompt($message, $options, $caseSensitive, $length, $withHeaders);
1154
1155        if (($exit) && ((strtolower($response) == 'n') || (strtolower($response) == 'no'))) {
1156            echo PHP_EOL;
1157            exit(127);
1158        }
1159
1160        return $response;
1161    }
1162
1163    /**
1164     * Colorize a string for output
1165     *
1166     * @param  string $string
1167     * @param  ?int   $fg
1168     * @param  ?int   $bg
1169     * @return string
1170     */
1171    public function colorize(string $string, ?int $fg = null, ?int $bg = null): string
1172    {
1173        return Color::colorize($string, $fg, $bg);
1174    }
1175
1176    /**
1177     * Append a string of text to the response body
1178     *
1179     * @param  ?string $text
1180     * @param  bool    $newline
1181     * @param  bool    $margin
1182     * @return Console
1183     */
1184    public function append(?string $text = null, bool $newline = true, bool $margin = true): Console
1185    {
1186        if (!empty($this->wrap)) {
1187            $lines = (strlen((string)$text) > $this->wrap) ?
1188                explode(PHP_EOL, wordwrap($text, $this->wrap, PHP_EOL)) : [$text];
1189        } else if (!empty($this->width)) {
1190            $lines = (strlen((string)$text) > ($this->width - ((int)$this->margin * 2))) ?
1191                explode(PHP_EOL, wordwrap($text, ($this->width - ((int)$this->margin * 2)), PHP_EOL)) : [$text];
1192        } else {
1193            $lines = [$text];
1194        }
1195
1196        foreach ($lines as $line) {
1197            $this->response .= (($margin) ? $this->getIndent() : '') . $line . (($newline) ? PHP_EOL : null);
1198        }
1199
1200        return $this;
1201    }
1202
1203    /**
1204     * Write a string of text to the response body and send the response
1205     *
1206     * @param  ?string $text
1207     * @param  bool    $newline
1208     * @param  bool    $margin
1209     * @param  bool    $withHeaders
1210     * @return Console
1211     */
1212    public function write(?string $text = null, bool $newline = true, bool $margin = true, bool $withHeaders = true): Console
1213    {
1214        $this->append($text, $newline, $margin);
1215        $this->send($withHeaders);
1216        return $this;
1217    }
1218
1219    /**
1220     * Send the response
1221     *
1222     * @param  bool $withHeaders
1223     * @return Console
1224     */
1225    public function send(bool $withHeaders = true): Console
1226    {
1227        if ($withHeaders) {
1228            if (($this->header !== null) && !($this->headerSent)) {
1229                $this->response = $this->formatTemplate($this->header) . $this->response;
1230            }
1231            if ($this->footer !== null) {
1232                $this->response .= $this->formatTemplate($this->footer);
1233            }
1234        }
1235
1236        echo $this->response;
1237        $this->response = null;
1238        return $this;
1239    }
1240
1241    /**
1242     * Display console help
1243     *
1244     * @param  bool    $raw
1245     * @param  ?string $subCommand
1246     * @return void
1247     */
1248    public function displayHelp(bool $raw = false, ?string $subCommand = null): void
1249    {
1250        $this->response = null;
1251
1252        if ($this->header !== null) {
1253            $this->response .= $this->formatTemplate($this->header);
1254        }
1255
1256        $this->response .= (new Help())->render($this->commands->all(), $raw, $subCommand, $this->getIndent(), $this->wrap, $this->helpColors);
1257
1258        if ($this->footer !== null) {
1259            $this->response .= $this->formatTemplate($this->footer);
1260        }
1261
1262        $this->send(false);
1263    }
1264
1265    /**
1266     * Clear the console
1267     *
1268     * @return void
1269     */
1270    public function clear(): void
1271    {
1272        echo chr(27) . "[2J" . chr(27) . "[;H";
1273    }
1274
1275    /**
1276     * Format header or footer template
1277     *
1278     * @param  string $template
1279     * @return string
1280     */
1281    protected function formatTemplate(string $template): string
1282    {
1283        $format = null;
1284
1285        if (str_contains($template, "\n")) {
1286            $templateLines = explode("\n", $template);
1287            foreach ($templateLines as $line) {
1288                $line    = trim($line);
1289                $format .= $this->getIndent() . $line . PHP_EOL;
1290            }
1291        } else {
1292            $format = $this->getIndent() . $template . PHP_EOL;
1293        }
1294
1295        return $format;
1296    }
1297
1298}