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