Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Help
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
6 / 6
36
100.00% covered (success)
100.00%
1 / 1
 render
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 matchesSubCommand
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 formatHelpLabel
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 colorizeHelpName
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 colorizeHelpParams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
11
 buildHelpRow
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
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
17/**
18 * Console help class
19 *
20 * @category   Pop
21 * @package    Pop\Console
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27class Help
28{
29
30    /**
31     * Render the help screen body for a set of commands
32     *
33     * @param  array   $commands
34     * @param  bool    $raw
35     * @param  ?string $subCommand
36     * @param  string  $indent
37     * @param  ?int    $wrap
38     * @param  array   $helpColors
39     * @return string
40     */
41    public function render(array $commands, bool $raw, ?string $subCommand, string $indent, ?int $wrap, array $helpColors): string
42    {
43        $response       = '';
44        $rows           = [];
45        $commandLengths = [];
46
47        foreach ($commands as $key => $command) {
48            if ($this->matchesSubCommand($command, $subCommand)) {
49                [$label, $length]     = $this->formatHelpLabel($command, $raw, $helpColors);
50                $rows[$key]           = $indent . $label;
51                $commandLengths[$key] = $length;
52            }
53        }
54
55        $maxLength = (!empty($commandLengths)) ? max($commandLengths) : 0;
56        $wrapped   = false;
57        $total     = count($rows);
58        $i         = 0;
59
60        foreach ($rows as $key => $row) {
61            [$line, $wrapped] = $this->buildHelpRow(
62                $commands[$key], $row, $commandLengths[$key], $maxLength, ($i == $total - 1), $wrapped, $indent, $wrap
63            );
64            $response .= $line;
65            $i++;
66        }
67
68        return $response;
69    }
70
71    /**
72     * Determine if a registered command belongs to the requested subcommand namespace
73     *
74     * Matches against the command's bare name with its registered script name (e.g. './app') stripped
75     * off first, rather than the raw display key, so a subcommand like 'db' or 'db:' correctly matches
76     * a command named 'db:migrate' without also matching an unrelated command registered under a script
77     * name that merely happens to start with the same letters (e.g. a script called 'dbapp'). Command
78     * naming conventions aren't assumed to use any particular delimiter (':', space, or otherwise) since
79     * the script name is stripped by exact, known value rather than guessed from string structure.
80     *
81     * @param  Command\CommandInterface $command
82     * @param  ?string                  $subCommand
83     * @return bool
84     */
85    protected function matchesSubCommand(Command\CommandInterface $command, ?string $subCommand = null): bool
86    {
87        if (empty($subCommand)) {
88            return true;
89        }
90
91        $name = (string)$command->getName();
92
93        if ($command->hasScriptName()) {
94            $prefix = $command->getScriptName() . ' ';
95            if (str_starts_with($name, $prefix)) {
96                $name = substr($name, strlen($prefix));
97            }
98        }
99
100        return str_starts_with($name, $subCommand);
101    }
102
103    /**
104     * Format a registered command's name/params into a colorized help label
105     *
106     * @param  Command\CommandInterface $command
107     * @param  bool                     $raw
108     * @param  array                    $helpColors
109     * @return array
110     */
111    protected function formatHelpLabel(Command\CommandInterface $command, bool $raw, array $helpColors): array
112    {
113        $name   = $command->getName();
114        $params = $command->getParams();
115        $length = strlen((string)$name);
116
117        if (count($helpColors) > 0) {
118            $name = $this->colorizeHelpName((string)$name, $raw, $helpColors);
119        }
120
121        if ($params !== null) {
122            $length += (strlen((string)$params) + 1);
123            $name   .= $this->colorizeHelpParams($params, $raw, $helpColors);
124        }
125
126        return [$name, $length];
127    }
128
129    /**
130     * Colorize a command name (and its sub-name, if space-separated) for the help screen
131     *
132     * @param  string $name
133     * @param  bool   $raw
134     * @param  array  $helpColors
135     * @return string
136     */
137    protected function colorizeHelpName(string $name, bool $raw, array $helpColors): string
138    {
139        if (str_contains($name, ' ')) {
140            $name1 = substr($name, 0, strpos($name, ' '));
141            $name2 = substr($name, strpos($name, ' ') + 1);
142            if (isset($helpColors[0])) {
143                $name1 = Color::colorize($name1, $helpColors[0], null, $raw);
144            }
145            if (isset($helpColors[1])) {
146                $name2 = Color::colorize($name2, $helpColors[1], null, $raw);
147            }
148            return $name1 . ' ' . $name2;
149        } else if (isset($helpColors[0])) {
150            return Color::colorize($name, $helpColors[0], null, $raw);
151        }
152
153        return $name;
154    }
155
156    /**
157     * Colorize a command's params for the help screen
158     *
159     * @param  string $params
160     * @param  bool   $raw
161     * @param  array  $helpColors
162     * @return string
163     */
164    protected function colorizeHelpParams(string $params, bool $raw, array $helpColors): string
165    {
166        if (str_contains($params, '-') && str_contains($params, '<')) {
167            $pars        = explode(' ', $params);
168            $optionFirst = str_contains($pars[0], '-');
169            $colorIndex  = 2;
170            $colored     = '';
171            foreach ($pars as $p) {
172                if (isset($helpColors[3]) &&
173                    (($optionFirst) && str_contains($p, '<')) || ((!$optionFirst) && str_contains($p, '-'))) {
174                    $colorIndex = 3;
175                }
176                $colored .= ' ' . ((isset($helpColors[$colorIndex])) ?
177                        Color::colorize($p, $helpColors[$colorIndex], null, $raw) : $p);
178            }
179            return $colored;
180        }
181
182        return ' ' . ((isset($helpColors[2])) ?
183                Color::colorize($params, $helpColors[2], null, $raw) : $params);
184    }
185
186    /**
187     * Build one command's row for the help screen, wrapping its help text if needed
188     *
189     * @param  Command\CommandInterface $command
190     * @param  string                   $label
191     * @param  int                      $length
192     * @param  int                      $maxLength
193     * @param  bool                     $isLast
194     * @param  bool                     $wrapped
195     * @param  string                   $indent
196     * @param  ?int                     $wrap
197     * @return array
198     */
199    protected function buildHelpRow(
200        Command\CommandInterface $command, string $label, int $length, int $maxLength, bool $isLast, bool $wrapped,
201        string $indent, ?int $wrap
202    ): array
203    {
204        if (!$command->hasHelp()) {
205            return [$label . $command->getHelp() . PHP_EOL, $wrapped];
206        }
207
208        $help = $command->getHelp();
209        $pad  = ($length < $maxLength) ?
210            str_repeat(' ', $maxLength - $length) . '    ' : '    ';
211
212        if (strlen((string)$command . $pad . $help) <= $wrap) {
213            return [$label . $pad . $help . PHP_EOL, false];
214        }
215
216        $row    = ($wrapped) ? '' : PHP_EOL;
217        $offset = $wrap - strlen((string)$command . $pad);
218        $lines  = explode(PHP_EOL, wordwrap($help, $offset, PHP_EOL));
219        foreach ($lines as $lineIndex => $line) {
220            $row .= ($lineIndex == 0) ?
221                $label . $pad . $line . PHP_EOL :
222                $indent . str_repeat(' ', strlen((string)$command)) . $pad . $line . PHP_EOL;
223        }
224
225        if (!$isLast) {
226            $row .= PHP_EOL;
227        }
228
229        return [$row, true];
230    }
231
232}