Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
77 / 77 |
|
92.55% |
87 / 94 |
|
16.50% |
33 / 200 |
|
73.68% |
14 / 19 |
CRAP | |
0.00% |
0 / 1 |
| Io | |
100.00% |
77 / 77 |
|
92.55% |
87 / 94 |
|
16.50% |
33 / 200 |
|
100.00% |
19 / 19 |
1171.11 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| line | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| write | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| success | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| warn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| error | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| stderr | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| escape | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| pad | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rule | |
100.00% |
7 / 7 |
|
90.00% |
9 / 10 |
|
33.33% |
4 / 12 |
|
100.00% |
1 / 1 |
8.74 | |||
| ask | |
100.00% |
6 / 6 |
|
88.89% |
8 / 9 |
|
37.50% |
3 / 8 |
|
100.00% |
1 / 1 |
5.20 | |||
| secret | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| confirm | |
100.00% |
4 / 4 |
|
88.89% |
8 / 9 |
|
50.00% |
3 / 6 |
|
100.00% |
1 / 1 |
4.12 | |||
| choice | |
100.00% |
16 / 16 |
|
90.48% |
19 / 21 |
|
7.14% |
4 / 56 |
|
100.00% |
1 / 1 |
73.85 | |||
| interactive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| width | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| output | |
100.00% |
3 / 3 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| indent | |
100.00% |
11 / 11 |
|
90.00% |
18 / 20 |
|
4.17% |
4 / 96 |
|
100.00% |
1 / 1 |
37.68 | |||
| wrap | |
100.00% |
15 / 15 |
|
100.00% |
10 / 10 |
|
12.50% |
1 / 8 |
|
100.00% |
1 / 1 |
21.75 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Console; |
| 6 | |
| 7 | use Stringable; |
| 8 | use ValueError; |
| 9 | |
| 10 | /** |
| 11 | * The output methods take a template and its arguments: the template is |
| 12 | * markup, e.g. `<green>done</green>` or `<strong>%d</strong>`, the |
| 13 | * arguments are data. They fill the template's sprintf() conversions |
| 14 | * and print as plain text, so no escaping is needed: |
| 15 | * |
| 16 | * $io->error('Cannot read <strong>%s</strong>', $path); |
| 17 | * |
| 18 | * Without arguments the template is not formatted, so `%` needs no |
| 19 | * doubling. Markup never fails: a tag without its partner prints |
| 20 | * literally. See Markup for the tag set. |
| 21 | * |
| 22 | * The terminal decides where the text goes and what it supports: the |
| 23 | * process streams by default, a Buffer in tests. |
| 24 | * |
| 25 | * @api |
| 26 | */ |
| 27 | final class Io |
| 28 | { |
| 29 | private readonly Markup $markup; |
| 30 | private bool $error = false; |
| 31 | |
| 32 | public function __construct( |
| 33 | private readonly Terminal $terminal = new Stdio(), |
| 34 | ) { |
| 35 | $this->markup = new Markup(); |
| 36 | } |
| 37 | |
| 38 | public function line(string $template = '', float|int|string|Stringable ...$args): void |
| 39 | { |
| 40 | $this->output($template, $args, PHP_EOL); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Like line(), without the line break. |
| 45 | */ |
| 46 | public function write(string $template, float|int|string|Stringable ...$args): void |
| 47 | { |
| 48 | $this->output($template, $args, ''); |
| 49 | } |
| 50 | |
| 51 | public function success(string $template, float|int|string|Stringable ...$args): void |
| 52 | { |
| 53 | $this->output($template, $args, PHP_EOL, style: 'green'); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Writes a yellow line to the error output. |
| 58 | */ |
| 59 | public function warn(string $template, float|int|string|Stringable ...$args): void |
| 60 | { |
| 61 | $this->output($template, $args, PHP_EOL, error: true, style: 'yellow'); |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Writes a red line to the error output. |
| 66 | */ |
| 67 | public function error(string $template, float|int|string|Stringable ...$args): void |
| 68 | { |
| 69 | $this->output($template, $args, PHP_EOL, error: true, style: 'red'); |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * An Io for the error output: whatever it writes goes there, plain |
| 74 | * lines and the prompts' questions included. |
| 75 | * |
| 76 | * $io->stderr()->line(' - %s', $path); |
| 77 | */ |
| 78 | public function stderr(): self |
| 79 | { |
| 80 | $io = clone $this; |
| 81 | $io->error = true; |
| 82 | |
| 83 | return $io; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Escapes markup tags and strips control characters (keeping |
| 88 | * newlines and tabs) so the text prints literally where it is |
| 89 | * concatenated into a template; arguments need no escaping. |
| 90 | * |
| 91 | * The result is meant for the output methods: it carries invisible |
| 92 | * markers that keep a `%` from reading as a sprintf() conversion and |
| 93 | * a tag after a trailing backslash from reading as escaped. |
| 94 | */ |
| 95 | public function escape(string $text): string |
| 96 | { |
| 97 | return $this->markup->escape($text); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Pads the text with spaces to the visible width `$width`; markup |
| 102 | * tags and multibyte characters don't count, wider text is |
| 103 | * returned unchanged. |
| 104 | */ |
| 105 | public function pad(string $text, int $width, Align $align = Align::Left): string |
| 106 | { |
| 107 | return $this->markup->pad($text, $width, $align); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Writes a horizontal rule: the char repeated across the terminal. |
| 112 | * |
| 113 | * `$max` caps the width like in `indent()`; a negative one counts as |
| 114 | * 0. The char may be a multi-char pattern and may carry markup — the |
| 115 | * repeat count uses its visible width: `$io->rule('<dim>─</dim>')` |
| 116 | * draws a dim line. |
| 117 | */ |
| 118 | public function rule(string $char = '─', ?int $max = null): void |
| 119 | { |
| 120 | $width = $this->terminal->width(); |
| 121 | |
| 122 | if ($max !== null && $max < $width) { |
| 123 | $width = max(0, $max); |
| 124 | } |
| 125 | |
| 126 | $unit = $this->markup->width($char); |
| 127 | |
| 128 | if ($unit < 1) { |
| 129 | throw new ValueError("Rule char '{$char}' has no visible width"); |
| 130 | } |
| 131 | |
| 132 | $this->line(str_repeat($char, intdiv($width, $unit))); |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Prints the question, and the default if there is one, and reads one |
| 137 | * line. A trimmed empty answer, or the end of input, yields the |
| 138 | * default. |
| 139 | */ |
| 140 | public function ask(string $question, string $default = ''): string |
| 141 | { |
| 142 | $this->write($question); |
| 143 | |
| 144 | if ($default !== '') { |
| 145 | $this->write(' [%s]', $default); |
| 146 | } |
| 147 | |
| 148 | $this->write(' '); |
| 149 | $answer = trim($this->terminal->read() ?? ''); |
| 150 | |
| 151 | return $answer === '' ? $default : $answer; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Asks for input that must not show while typing, such as a password. |
| 156 | * |
| 157 | * The answer keeps its whitespace. A terminal that fails to hide the |
| 158 | * input throws rather than reading visibly; Stdio has no way to hide |
| 159 | * it on Windows and reads visibly there, see Stdio::read(). |
| 160 | */ |
| 161 | public function secret(string $question): string |
| 162 | { |
| 163 | $this->write($question . ' '); |
| 164 | |
| 165 | return $this->terminal->read(hidden: true) ?? ''; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Asks a yes/no question and returns the answer as bool. |
| 170 | * |
| 171 | * An empty answer yields the default; an answer starting with `y` or |
| 172 | * `Y` means yes, anything else no. |
| 173 | */ |
| 174 | public function confirm(string $question, bool $default = false): bool |
| 175 | { |
| 176 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 177 | |
| 178 | if ($answer === '') { |
| 179 | return $default; |
| 180 | } |
| 181 | |
| 182 | return str_starts_with($answer, 'y'); |
| 183 | } |
| 184 | |
| 185 | /** |
| 186 | * Asks to pick one of the options and returns its key. |
| 187 | * |
| 188 | * The labels are listed numbered from 1, and the prompt shows the |
| 189 | * default's number: `[1]`. An empty answer, or the end of input, |
| 190 | * yields the default, the first option unless given; any other answer |
| 191 | * must be a listed number — an invalid one asks again. |
| 192 | * |
| 193 | * @template K of array-key |
| 194 | * |
| 195 | * @param array<K, string> $options |
| 196 | * @param K|null $default |
| 197 | * |
| 198 | * @return K |
| 199 | */ |
| 200 | public function choice(string $question, array $options, int|string|null $default = null): int|string |
| 201 | { |
| 202 | if ($options === []) { |
| 203 | throw new ValueError('Choice needs options'); |
| 204 | } |
| 205 | |
| 206 | $default ??= array_key_first($options); |
| 207 | |
| 208 | if (!array_key_exists($default, $options)) { |
| 209 | throw new ValueError("Choice default '{$default}' is not an option"); |
| 210 | } |
| 211 | |
| 212 | $keys = array_keys($options); |
| 213 | $number = array_flip($keys)[$default] + 1; |
| 214 | $this->line($question); |
| 215 | |
| 216 | foreach (array_values($options) as $i => $label) { |
| 217 | $this->line(' %d) %s', $i + 1, $label); |
| 218 | } |
| 219 | |
| 220 | while (true) { |
| 221 | $answer = $this->ask("[{$number}]"); |
| 222 | |
| 223 | if ($answer === '') { |
| 224 | return $keys[$number - 1]; |
| 225 | } |
| 226 | |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 228 | return $keys[(int) $answer - 1]; |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Whether someone can see the prompts and answer them. |
| 235 | */ |
| 236 | public function interactive(): bool |
| 237 | { |
| 238 | return $this->terminal->interactive(); |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * The terminal width in columns. |
| 243 | */ |
| 244 | public function width(): int |
| 245 | { |
| 246 | return $this->terminal->width(); |
| 247 | } |
| 248 | |
| 249 | /** @param array<float|int|string|Stringable> $args */ |
| 250 | private function output(string $template, array $args, string $end, bool $error = false, string $style = ''): void |
| 251 | { |
| 252 | $error = $error || $this->error; |
| 253 | $text = $this->markup->render($template, array_values($args), $this->terminal->colors($error), $style); |
| 254 | $this->terminal->write($text . $end, $error); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Indents the text and wraps it on its visible width; markup tags |
| 259 | * and multibyte characters don't count. `$max` caps the total line |
| 260 | * width: the text wraps as if the terminal were at most that wide. |
| 261 | * A negative `$indent` counts as 0. |
| 262 | */ |
| 263 | public function indent( |
| 264 | string $text, |
| 265 | int $indent, |
| 266 | ?int $max = null, |
| 267 | ): string { |
| 268 | $indent = max(0, $indent); |
| 269 | $spaces = str_repeat(' ', $indent); |
| 270 | $terminal = $this->terminal->width(); |
| 271 | |
| 272 | if ($max !== null && $max < $terminal) { |
| 273 | $terminal = $max; |
| 274 | } |
| 275 | |
| 276 | $width = $terminal - $indent; |
| 277 | |
| 278 | $lines = []; |
| 279 | |
| 280 | foreach (explode("\n", $text) as $line) { |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | return implode("\n", $lines); |
| 287 | } |
| 288 | |
| 289 | /** |
| 290 | * Wraps one line at spaces; a word longer than the width overflows. |
| 291 | * |
| 292 | * @return list<string> |
| 293 | */ |
| 294 | private function wrap(string $line, int $width): array |
| 295 | { |
| 296 | $lines = []; |
| 297 | $current = null; |
| 298 | $currentWidth = 0; |
| 299 | |
| 300 | foreach (explode(' ', $line) as $word) { |
| 301 | $wordWidth = $this->markup->width($word); |
| 302 | |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 304 | $current .= ' ' . $word; |
| 305 | $currentWidth += 1 + $wordWidth; |
| 306 | |
| 307 | continue; |
| 308 | } |
| 309 | |
| 310 | if ($current !== null) { |
| 311 | $lines[] = $current; |
| 312 | } |
| 313 | |
| 314 | $current = $word; |
| 315 | $currentWidth = $wordWidth; |
| 316 | } |
| 317 | |
| 318 | $lines[] = (string) $current; |
| 319 | |
| 320 | return $lines; |
| 321 | } |
| 322 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 33 | private readonly Terminal $terminal = new Stdio(), |
| 34 | ) { |
| 35 | $this->markup = new Markup(); |
| 36 | } |
| 140 | public function ask(string $question, string $default = ''): string |
| 141 | { |
| 142 | $this->write($question); |
| 143 | |
| 144 | if ($default !== '') { |
| 145 | $this->write(' [%s]', $default); |
| 146 | } |
| 147 | |
| 148 | $this->write(' '); |
| 148 | $this->write(' '); |
| 149 | $answer = trim($this->terminal->read() ?? ''); |
| 149 | $answer = trim($this->terminal->read() ?? ''); |
| 149 | $answer = trim($this->terminal->read() ?? ''); |
| 149 | $answer = trim($this->terminal->read() ?? ''); |
| 150 | |
| 151 | return $answer === '' ? $default : $answer; |
| 151 | return $answer === '' ? $default : $answer; |
| 151 | return $answer === '' ? $default : $answer; |
| 151 | return $answer === '' ? $default : $answer; |
| 152 | } |
| 200 | public function choice(string $question, array $options, int|string|null $default = null): int|string |
| 201 | { |
| 202 | if ($options === []) { |
| 203 | throw new ValueError('Choice needs options'); |
| 206 | $default ??= array_key_first($options); |
| 207 | |
| 208 | if (!array_key_exists($default, $options)) { |
| 209 | throw new ValueError("Choice default '{$default}' is not an option"); |
| 212 | $keys = array_keys($options); |
| 213 | $number = array_flip($keys)[$default] + 1; |
| 214 | $this->line($question); |
| 215 | |
| 216 | foreach (array_values($options) as $i => $label) { |
| 216 | foreach (array_values($options) as $i => $label) { |
| 216 | foreach (array_values($options) as $i => $label) { |
| 216 | foreach (array_values($options) as $i => $label) { |
| 217 | $this->line(' %d) %s', $i + 1, $label); |
| 218 | } |
| 219 | |
| 220 | while (true) { |
| 221 | $answer = $this->ask("[{$number}]"); |
| 222 | |
| 223 | if ($answer === '') { |
| 224 | return $keys[$number - 1]; |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 227 | if (preg_match('/^\d+\z/', $answer) === 1 && (int) $answer >= 1 && (int) $answer <= count($keys)) { |
| 228 | return $keys[(int) $answer - 1]; |
| 220 | while (true) { |
| 231 | } |
| 174 | public function confirm(string $question, bool $default = false): bool |
| 175 | { |
| 176 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 176 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 176 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 176 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 177 | |
| 178 | if ($answer === '') { |
| 179 | return $default; |
| 182 | return str_starts_with($answer, 'y'); |
| 182 | return str_starts_with($answer, 'y'); |
| 182 | return str_starts_with($answer, 'y'); |
| 182 | return str_starts_with($answer, 'y'); |
| 183 | } |
| 67 | public function error(string $template, float|int|string|Stringable ...$args): void |
| 68 | { |
| 69 | $this->output($template, $args, PHP_EOL, error: true, style: 'red'); |
| 70 | } |
| 95 | public function escape(string $text): string |
| 96 | { |
| 97 | return $this->markup->escape($text); |
| 98 | } |
| 264 | string $text, |
| 265 | int $indent, |
| 266 | ?int $max = null, |
| 267 | ): string { |
| 268 | $indent = max(0, $indent); |
| 268 | $indent = max(0, $indent); |
| 268 | $indent = max(0, $indent); |
| 268 | $indent = max(0, $indent); |
| 269 | $spaces = str_repeat(' ', $indent); |
| 270 | $terminal = $this->terminal->width(); |
| 271 | |
| 272 | if ($max !== null && $max < $terminal) { |
| 272 | if ($max !== null && $max < $terminal) { |
| 272 | if ($max !== null && $max < $terminal) { |
| 273 | $terminal = $max; |
| 274 | } |
| 275 | |
| 276 | $width = $terminal - $indent; |
| 276 | $width = $terminal - $indent; |
| 277 | |
| 278 | $lines = []; |
| 279 | |
| 280 | foreach (explode("\n", $text) as $line) { |
| 280 | foreach (explode("\n", $text) as $line) { |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 280 | foreach (explode("\n", $text) as $line) { |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 280 | foreach (explode("\n", $text) as $line) { |
| 281 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 282 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 283 | } |
| 284 | } |
| 285 | |
| 286 | return implode("\n", $lines); |
| 286 | return implode("\n", $lines); |
| 286 | return implode("\n", $lines); |
| 286 | return implode("\n", $lines); |
| 287 | } |
| 238 | return $this->terminal->interactive(); |
| 239 | } |
| 38 | public function line(string $template = '', float|int|string|Stringable ...$args): void |
| 39 | { |
| 40 | $this->output($template, $args, PHP_EOL); |
| 41 | } |
| 250 | private function output(string $template, array $args, string $end, bool $error = false, string $style = ''): void |
| 251 | { |
| 252 | $error = $error || $this->error; |
| 252 | $error = $error || $this->error; |
| 252 | $error = $error || $this->error; |
| 253 | $text = $this->markup->render($template, array_values($args), $this->terminal->colors($error), $style); |
| 254 | $this->terminal->write($text . $end, $error); |
| 255 | } |
| 105 | public function pad(string $text, int $width, Align $align = Align::Left): string |
| 106 | { |
| 107 | return $this->markup->pad($text, $width, $align); |
| 108 | } |
| 118 | public function rule(string $char = '─', ?int $max = null): void |
| 119 | { |
| 120 | $width = $this->terminal->width(); |
| 121 | |
| 122 | if ($max !== null && $max < $width) { |
| 122 | if ($max !== null && $max < $width) { |
| 122 | if ($max !== null && $max < $width) { |
| 123 | $width = max(0, $max); |
| 123 | $width = max(0, $max); |
| 123 | $width = max(0, $max); |
| 123 | $width = max(0, $max); |
| 124 | } |
| 125 | |
| 126 | $unit = $this->markup->width($char); |
| 126 | $unit = $this->markup->width($char); |
| 127 | |
| 128 | if ($unit < 1) { |
| 129 | throw new ValueError("Rule char '{$char}' has no visible width"); |
| 132 | $this->line(str_repeat($char, intdiv($width, $unit))); |
| 133 | } |
| 161 | public function secret(string $question): string |
| 162 | { |
| 163 | $this->write($question . ' '); |
| 164 | |
| 165 | return $this->terminal->read(hidden: true) ?? ''; |
| 166 | } |
| 80 | $io = clone $this; |
| 81 | $io->error = true; |
| 82 | |
| 83 | return $io; |
| 84 | } |
| 51 | public function success(string $template, float|int|string|Stringable ...$args): void |
| 52 | { |
| 53 | $this->output($template, $args, PHP_EOL, style: 'green'); |
| 54 | } |
| 59 | public function warn(string $template, float|int|string|Stringable ...$args): void |
| 60 | { |
| 61 | $this->output($template, $args, PHP_EOL, error: true, style: 'yellow'); |
| 62 | } |
| 246 | return $this->terminal->width(); |
| 247 | } |
| 294 | private function wrap(string $line, int $width): array |
| 295 | { |
| 296 | $lines = []; |
| 297 | $current = null; |
| 298 | $currentWidth = 0; |
| 299 | |
| 300 | foreach (explode(' ', $line) as $word) { |
| 300 | foreach (explode(' ', $line) as $word) { |
| 301 | $wordWidth = $this->markup->width($word); |
| 302 | |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 304 | $current .= ' ' . $word; |
| 305 | $currentWidth += 1 + $wordWidth; |
| 306 | |
| 307 | continue; |
| 310 | if ($current !== null) { |
| 311 | $lines[] = $current; |
| 312 | } |
| 313 | |
| 314 | $current = $word; |
| 300 | foreach (explode(' ', $line) as $word) { |
| 301 | $wordWidth = $this->markup->width($word); |
| 302 | |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 304 | $current .= ' ' . $word; |
| 305 | $currentWidth += 1 + $wordWidth; |
| 306 | |
| 307 | continue; |
| 308 | } |
| 309 | |
| 310 | if ($current !== null) { |
| 311 | $lines[] = $current; |
| 312 | } |
| 313 | |
| 314 | $current = $word; |
| 300 | foreach (explode(' ', $line) as $word) { |
| 301 | $wordWidth = $this->markup->width($word); |
| 302 | |
| 303 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 304 | $current .= ' ' . $word; |
| 305 | $currentWidth += 1 + $wordWidth; |
| 306 | |
| 307 | continue; |
| 308 | } |
| 309 | |
| 310 | if ($current !== null) { |
| 311 | $lines[] = $current; |
| 312 | } |
| 313 | |
| 314 | $current = $word; |
| 315 | $currentWidth = $wordWidth; |
| 316 | } |
| 317 | |
| 318 | $lines[] = (string) $current; |
| 319 | |
| 320 | return $lines; |
| 321 | } |
| 46 | public function write(string $template, float|int|string|Stringable ...$args): void |
| 47 | { |
| 48 | $this->output($template, $args, ''); |
| 49 | } |