Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
82 / 82 |
|
96.91% |
94 / 97 |
|
34.26% |
37 / 108 |
|
86.96% |
20 / 23 |
CRAP | |
0.00% |
0 / 1 |
| Io | |
100.00% |
82 / 82 |
|
96.91% |
94 / 97 |
|
34.26% |
37 / 108 |
|
100.00% |
23 / 23 |
914.47 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| echo | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| echoln | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| echoErr | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| echolnErr | |
100.00% |
2 / 2 |
|
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 | |||
| info | |
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 | |||
| rule | |
100.00% |
7 / 7 |
|
100.00% |
7 / 7 |
|
50.00% |
4 / 8 |
|
100.00% |
1 / 1 |
6.00 | |||
| ask | |
100.00% |
4 / 4 |
|
90.00% |
9 / 10 |
|
50.00% |
3 / 6 |
|
100.00% |
1 / 1 |
4.12 | |||
| confirm | |
100.00% |
4 / 4 |
|
88.89% |
8 / 9 |
|
50.00% |
3 / 6 |
|
100.00% |
1 / 1 |
4.12 | |||
| readline | |
100.00% |
3 / 3 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
8.12 | |||
| write | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| indent | |
100.00% |
10 / 10 |
|
94.12% |
16 / 17 |
|
8.33% |
4 / 48 |
|
100.00% |
1 / 1 |
33.73 | |||
| wrap | |
100.00% |
15 / 15 |
|
100.00% |
10 / 10 |
|
12.50% |
1 / 8 |
|
100.00% |
1 / 1 |
21.75 | |||
| terminalWidth | |
100.00% |
6 / 6 |
|
100.00% |
9 / 9 |
|
40.00% |
2 / 5 |
|
100.00% |
1 / 1 |
13.78 | |||
| stdout | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| stderr | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| stdin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| open | |
100.00% |
6 / 6 |
|
100.00% |
6 / 6 |
|
0.00% |
0 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| hasColorSupport | |
100.00% |
8 / 8 |
|
100.00% |
9 / 9 |
|
66.67% |
4 / 6 |
|
100.00% |
1 / 1 |
8.81 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celema\Console; |
| 6 | |
| 7 | use RuntimeException; |
| 8 | use ValueError; |
| 9 | |
| 10 | /** |
| 11 | * The echo methods render the inline console markup, e.g. |
| 12 | * `<green>done</green>` or `<strong>1</strong>`; see Markup for the tag |
| 13 | * set and passthrough rules. Use `escape()` for text that must print |
| 14 | * literally. The message helpers treat their input as plain text. |
| 15 | * |
| 16 | * @api |
| 17 | */ |
| 18 | class Io |
| 19 | { |
| 20 | private readonly Markup $markup; |
| 21 | private mixed $stream = null; |
| 22 | private mixed $errorStream = null; |
| 23 | private mixed $inputStream = null; |
| 24 | private ?int $width = null; |
| 25 | |
| 26 | public function __construct( |
| 27 | protected readonly string $target = 'php://stdout', |
| 28 | protected readonly string $errorTarget = 'php://stderr', |
| 29 | protected readonly string $inputTarget = 'php://stdin', |
| 30 | ) { |
| 31 | $this->markup = new Markup(); |
| 32 | } |
| 33 | |
| 34 | public function echo(string $text): void |
| 35 | { |
| 36 | $stream = $this->stdout(); |
| 37 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream))); |
| 38 | } |
| 39 | |
| 40 | public function echoln(string $text): void |
| 41 | { |
| 42 | $stream = $this->stdout(); |
| 43 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream)) . PHP_EOL); |
| 44 | } |
| 45 | |
| 46 | public function echoErr(string $text): void |
| 47 | { |
| 48 | $stream = $this->stderr(); |
| 49 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream))); |
| 50 | } |
| 51 | |
| 52 | public function echolnErr(string $text): void |
| 53 | { |
| 54 | $stream = $this->stderr(); |
| 55 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream)) . PHP_EOL); |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * Escapes markup tags and strips control characters (keeping |
| 60 | * newlines and tabs) so the text prints literally. |
| 61 | */ |
| 62 | public function escape(string $text): string |
| 63 | { |
| 64 | return $this->markup->escape($text); |
| 65 | } |
| 66 | |
| 67 | public function info(string $message): void |
| 68 | { |
| 69 | $this->echoln($this->escape($message)); |
| 70 | } |
| 71 | |
| 72 | public function success(string $message): void |
| 73 | { |
| 74 | $this->echoln('<green>' . $this->escape($message) . '</green>'); |
| 75 | } |
| 76 | |
| 77 | public function warn(string $message): void |
| 78 | { |
| 79 | $this->echolnErr('<yellow>' . $this->escape($message) . '</yellow>'); |
| 80 | } |
| 81 | |
| 82 | public function error(string $message): void |
| 83 | { |
| 84 | $this->echolnErr('<red>' . $this->escape($message) . '</red>'); |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Writes a horizontal rule: the char repeated across the terminal. |
| 89 | * |
| 90 | * `$max` caps the width like in `indent()`. The char may be a |
| 91 | * multi-char pattern and may carry markup — the repeat count uses |
| 92 | * its visible width: `$io->rule('<dim>─</dim>')` draws a dim line. |
| 93 | */ |
| 94 | public function rule(string $char = '─', ?int $max = null): void |
| 95 | { |
| 96 | $width = $this->terminalWidth(); |
| 97 | |
| 98 | if ($max !== null && $max < $width) { |
| 99 | $width = $max; |
| 100 | } |
| 101 | |
| 102 | $unit = $this->markup->width($char); |
| 103 | |
| 104 | if ($unit < 1) { |
| 105 | throw new ValueError("Rule char '{$char}' has no visible width"); |
| 106 | } |
| 107 | |
| 108 | $this->echoln(str_repeat($char, intdiv($width, $unit))); |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Prints the question and reads one line from the input stream. |
| 113 | * |
| 114 | * A trimmed empty answer (or end of input) yields the default. With |
| 115 | * `hidden` the terminal echo is switched off while typing, for example |
| 116 | * for passwords, and the answer keeps its whitespace; only the trailing |
| 117 | * newline is stripped. On Windows, or without a terminal, the input is |
| 118 | * simply read as is, visibly. |
| 119 | */ |
| 120 | public function ask(string $question, string $default = '', bool $hidden = false): string |
| 121 | { |
| 122 | $this->echo($question . ' '); |
| 123 | $line = $this->readline($hidden); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 125 | |
| 126 | return $answer === '' ? $default : $answer; |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * Asks a yes/no question and returns the answer as bool. |
| 131 | * |
| 132 | * An empty answer yields the default; an answer starting with `y` or |
| 133 | * `Y` means yes, anything else no. |
| 134 | */ |
| 135 | public function confirm(string $question, bool $default = false): bool |
| 136 | { |
| 137 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 138 | |
| 139 | if ($answer === '') { |
| 140 | return $default; |
| 141 | } |
| 142 | |
| 143 | return str_starts_with($answer, 'y'); |
| 144 | } |
| 145 | |
| 146 | private function readline(bool $hidden): string |
| 147 | { |
| 148 | $stream = $this->stdin(); |
| 149 | |
| 150 | // No stty on Windows; shell_exec would leak its error output. |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 152 | // @codeCoverageIgnoreStart |
| 153 | /** @psalm-suppress ForbiddenCode */ |
| 154 | $previous = trim((string) shell_exec('stty -g')); |
| 155 | |
| 156 | /** @psalm-suppress ForbiddenCode */ |
| 157 | shell_exec('stty -echo'); |
| 158 | |
| 159 | try { |
| 160 | return (string) fgets($stream); |
| 161 | } finally { |
| 162 | // Restore the saved terminal state rather than assuming |
| 163 | // echo was on, even when reading throws. |
| 164 | /** @psalm-suppress ForbiddenCode */ |
| 165 | shell_exec($previous === '' ? 'stty echo' : 'stty ' . escapeshellarg($previous)); |
| 166 | $this->echo(PHP_EOL); |
| 167 | } |
| 168 | |
| 169 | // @codeCoverageIgnoreEnd |
| 170 | } |
| 171 | |
| 172 | return (string) fgets($stream); |
| 173 | } |
| 174 | |
| 175 | private function write(mixed $stream, string $text): void |
| 176 | { |
| 177 | fwrite($stream, $text); |
| 178 | fflush($stream); |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Indents the text and wraps it on its visible width; markup tags |
| 183 | * and multibyte characters don't count. `$max` caps the total line |
| 184 | * width: the text wraps as if the terminal were at most that wide. |
| 185 | */ |
| 186 | public function indent( |
| 187 | string $text, |
| 188 | int $indent, |
| 189 | ?int $max = null, |
| 190 | ): string { |
| 191 | $spaces = str_repeat(' ', $indent); |
| 192 | $terminal = $this->terminalWidth(); |
| 193 | |
| 194 | if ($max !== null && $max < $terminal) { |
| 195 | $terminal = $max; |
| 196 | } |
| 197 | |
| 198 | $width = $terminal - $indent; |
| 199 | |
| 200 | $lines = []; |
| 201 | |
| 202 | foreach (explode("\n", $text) as $line) { |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | return implode("\n", $lines); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * Wraps one line at spaces; a word longer than the width overflows. |
| 213 | * |
| 214 | * @return list<string> |
| 215 | */ |
| 216 | private function wrap(string $line, int $width): array |
| 217 | { |
| 218 | $lines = []; |
| 219 | $current = null; |
| 220 | $currentWidth = 0; |
| 221 | |
| 222 | foreach (explode(' ', $line) as $word) { |
| 223 | $wordWidth = $this->markup->width($word); |
| 224 | |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 226 | $current .= ' ' . $word; |
| 227 | $currentWidth += 1 + $wordWidth; |
| 228 | |
| 229 | continue; |
| 230 | } |
| 231 | |
| 232 | if ($current !== null) { |
| 233 | $lines[] = $current; |
| 234 | } |
| 235 | |
| 236 | $current = $word; |
| 237 | $currentWidth = $wordWidth; |
| 238 | } |
| 239 | |
| 240 | $lines[] = (string) $current; |
| 241 | |
| 242 | return $lines; |
| 243 | } |
| 244 | |
| 245 | private function terminalWidth(): int |
| 246 | { |
| 247 | if ($this->width !== null) { |
| 248 | return $this->width; |
| 249 | } |
| 250 | |
| 251 | $columns = (int) getenv('COLUMNS'); |
| 252 | |
| 253 | // No tput on Windows; shell_exec would leak its error output. |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 255 | // @codeCoverageIgnoreStart |
| 256 | /** @psalm-suppress ForbiddenCode */ |
| 257 | $columns = (int) shell_exec('tput cols'); |
| 258 | |
| 259 | // @codeCoverageIgnoreEnd |
| 260 | } |
| 261 | |
| 262 | if ($columns < 1) { |
| 263 | // @codeCoverageIgnoreStart |
| 264 | $columns = 80; |
| 265 | |
| 266 | // @codeCoverageIgnoreEnd |
| 267 | } |
| 268 | |
| 269 | return $this->width = $columns; |
| 270 | } |
| 271 | |
| 272 | protected function stdout(): mixed |
| 273 | { |
| 274 | return $this->stream ??= $this->open($this->target, 'w'); |
| 275 | } |
| 276 | |
| 277 | protected function stderr(): mixed |
| 278 | { |
| 279 | return $this->errorStream ??= $this->open($this->errorTarget, 'w'); |
| 280 | } |
| 281 | |
| 282 | protected function stdin(): mixed |
| 283 | { |
| 284 | return $this->inputStream ??= $this->open($this->inputTarget, 'r'); |
| 285 | } |
| 286 | |
| 287 | private function open(string $target, string $mode): mixed |
| 288 | { |
| 289 | set_error_handler(static fn(): bool => true); |
| 290 | |
| 291 | try { |
| 292 | $stream = fopen($target, $mode); |
| 293 | } finally { |
| 294 | restore_error_handler(); |
| 295 | } |
| 296 | |
| 297 | if ($stream === false) { |
| 298 | throw new RuntimeException("Could not open stream '{$target}'"); |
| 299 | } |
| 300 | |
| 301 | return $stream; |
| 302 | } |
| 303 | |
| 304 | protected function hasColorSupport(mixed $stream): bool |
| 305 | { |
| 306 | $noColor = getenv('NO_COLOR'); |
| 307 | |
| 308 | if ($noColor !== false && $noColor !== '') { |
| 309 | return false; |
| 310 | } |
| 311 | |
| 312 | $force = getenv('FORCE_COLOR'); |
| 313 | |
| 314 | if ($force !== false) { |
| 315 | return $force !== '0' && strtolower($force) !== 'false'; |
| 316 | } |
| 317 | |
| 318 | $terminal = stream_isatty($stream); |
| 319 | |
| 320 | // @codeCoverageIgnoreStart |
| 321 | if (DIRECTORY_SEPARATOR === '\\' && $terminal) { |
| 322 | // VT100 processing is off by default in cmd/PowerShell; |
| 323 | // enabling it reports whether the console supports it. |
| 324 | return sapi_windows_vt100_support($stream, enable: true); |
| 325 | } |
| 326 | |
| 327 | // @codeCoverageIgnoreEnd |
| 328 | |
| 329 | return $terminal; |
| 330 | } |
| 331 | } |
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.
| 27 | protected readonly string $target = 'php://stdout', |
| 28 | protected readonly string $errorTarget = 'php://stderr', |
| 29 | protected readonly string $inputTarget = 'php://stdin', |
| 30 | ) { |
| 31 | $this->markup = new Markup(); |
| 32 | } |
| 120 | public function ask(string $question, string $default = '', bool $hidden = false): string |
| 121 | { |
| 122 | $this->echo($question . ' '); |
| 123 | $line = $this->readline($hidden); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 124 | $answer = $hidden ? rtrim($line, characters: "\r\n") : trim($line); |
| 125 | |
| 126 | return $answer === '' ? $default : $answer; |
| 126 | return $answer === '' ? $default : $answer; |
| 126 | return $answer === '' ? $default : $answer; |
| 126 | return $answer === '' ? $default : $answer; |
| 127 | } |
| 135 | public function confirm(string $question, bool $default = false): bool |
| 136 | { |
| 137 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 137 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 137 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 137 | $answer = strtolower($this->ask($question . ($default ? ' [Y/n]' : ' [y/N]'))); |
| 138 | |
| 139 | if ($answer === '') { |
| 140 | return $default; |
| 143 | return str_starts_with($answer, 'y'); |
| 143 | return str_starts_with($answer, 'y'); |
| 143 | return str_starts_with($answer, 'y'); |
| 143 | return str_starts_with($answer, 'y'); |
| 144 | } |
| 34 | public function echo(string $text): void |
| 35 | { |
| 36 | $stream = $this->stdout(); |
| 37 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream))); |
| 38 | } |
| 46 | public function echoErr(string $text): void |
| 47 | { |
| 48 | $stream = $this->stderr(); |
| 49 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream))); |
| 50 | } |
| 40 | public function echoln(string $text): void |
| 41 | { |
| 42 | $stream = $this->stdout(); |
| 43 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream)) . PHP_EOL); |
| 44 | } |
| 52 | public function echolnErr(string $text): void |
| 53 | { |
| 54 | $stream = $this->stderr(); |
| 55 | $this->write($stream, $this->markup->render($text, $this->hasColorSupport($stream)) . PHP_EOL); |
| 56 | } |
| 82 | public function error(string $message): void |
| 83 | { |
| 84 | $this->echolnErr('<red>' . $this->escape($message) . '</red>'); |
| 85 | } |
| 62 | public function escape(string $text): string |
| 63 | { |
| 64 | return $this->markup->escape($text); |
| 65 | } |
| 304 | protected function hasColorSupport(mixed $stream): bool |
| 305 | { |
| 306 | $noColor = getenv('NO_COLOR'); |
| 307 | |
| 308 | if ($noColor !== false && $noColor !== '') { |
| 308 | if ($noColor !== false && $noColor !== '') { |
| 308 | if ($noColor !== false && $noColor !== '') { |
| 309 | return false; |
| 312 | $force = getenv('FORCE_COLOR'); |
| 313 | |
| 314 | if ($force !== false) { |
| 315 | return $force !== '0' && strtolower($force) !== 'false'; |
| 315 | return $force !== '0' && strtolower($force) !== 'false'; |
| 315 | return $force !== '0' && strtolower($force) !== 'false'; |
| 329 | return $terminal; |
| 330 | } |
| 187 | string $text, |
| 188 | int $indent, |
| 189 | ?int $max = null, |
| 190 | ): string { |
| 191 | $spaces = str_repeat(' ', $indent); |
| 192 | $terminal = $this->terminalWidth(); |
| 193 | |
| 194 | if ($max !== null && $max < $terminal) { |
| 194 | if ($max !== null && $max < $terminal) { |
| 194 | if ($max !== null && $max < $terminal) { |
| 195 | $terminal = $max; |
| 196 | } |
| 197 | |
| 198 | $width = $terminal - $indent; |
| 198 | $width = $terminal - $indent; |
| 199 | |
| 200 | $lines = []; |
| 201 | |
| 202 | foreach (explode("\n", $text) as $line) { |
| 202 | foreach (explode("\n", $text) as $line) { |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 202 | foreach (explode("\n", $text) as $line) { |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 202 | foreach (explode("\n", $text) as $line) { |
| 203 | foreach ($this->wrap($line, $width) as $wrapped) { |
| 204 | $lines[] = $wrapped === '' ? '' : $spaces . $wrapped; |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | return implode("\n", $lines); |
| 208 | return implode("\n", $lines); |
| 208 | return implode("\n", $lines); |
| 208 | return implode("\n", $lines); |
| 209 | } |
| 67 | public function info(string $message): void |
| 68 | { |
| 69 | $this->echoln($this->escape($message)); |
| 70 | } |
| 287 | private function open(string $target, string $mode): mixed |
| 288 | { |
| 289 | set_error_handler(static fn(): bool => true); |
| 290 | |
| 291 | try { |
| 292 | $stream = fopen($target, $mode); |
| 293 | } finally { |
| 293 | } finally { |
| 294 | restore_error_handler(); |
| 297 | if ($stream === false) { |
| 298 | throw new RuntimeException("Could not open stream '{$target}'"); |
| 301 | return $stream; |
| 302 | } |
| 146 | private function readline(bool $hidden): string |
| 147 | { |
| 148 | $stream = $this->stdin(); |
| 149 | |
| 150 | // No stty on Windows; shell_exec would leak its error output. |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 151 | if ($hidden && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($stream)) { |
| 172 | return (string) fgets($stream); |
| 173 | } |
| 94 | public function rule(string $char = '─', ?int $max = null): void |
| 95 | { |
| 96 | $width = $this->terminalWidth(); |
| 97 | |
| 98 | if ($max !== null && $max < $width) { |
| 98 | if ($max !== null && $max < $width) { |
| 98 | if ($max !== null && $max < $width) { |
| 99 | $width = $max; |
| 100 | } |
| 101 | |
| 102 | $unit = $this->markup->width($char); |
| 102 | $unit = $this->markup->width($char); |
| 103 | |
| 104 | if ($unit < 1) { |
| 105 | throw new ValueError("Rule char '{$char}' has no visible width"); |
| 108 | $this->echoln(str_repeat($char, intdiv($width, $unit))); |
| 109 | } |
| 279 | return $this->errorStream ??= $this->open($this->errorTarget, 'w'); |
| 280 | } |
| 284 | return $this->inputStream ??= $this->open($this->inputTarget, 'r'); |
| 285 | } |
| 274 | return $this->stream ??= $this->open($this->target, 'w'); |
| 275 | } |
| 72 | public function success(string $message): void |
| 73 | { |
| 74 | $this->echoln('<green>' . $this->escape($message) . '</green>'); |
| 75 | } |
| 247 | if ($this->width !== null) { |
| 248 | return $this->width; |
| 251 | $columns = (int) getenv('COLUMNS'); |
| 252 | |
| 253 | // No tput on Windows; shell_exec would leak its error output. |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 254 | if ($columns < 1 && DIRECTORY_SEPARATOR !== '\\' && stream_isatty($this->stdout())) { |
| 262 | if ($columns < 1) { |
| 269 | return $this->width = $columns; |
| 270 | } |
| 77 | public function warn(string $message): void |
| 78 | { |
| 79 | $this->echolnErr('<yellow>' . $this->escape($message) . '</yellow>'); |
| 80 | } |
| 216 | private function wrap(string $line, int $width): array |
| 217 | { |
| 218 | $lines = []; |
| 219 | $current = null; |
| 220 | $currentWidth = 0; |
| 221 | |
| 222 | foreach (explode(' ', $line) as $word) { |
| 222 | foreach (explode(' ', $line) as $word) { |
| 223 | $wordWidth = $this->markup->width($word); |
| 224 | |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 226 | $current .= ' ' . $word; |
| 227 | $currentWidth += 1 + $wordWidth; |
| 228 | |
| 229 | continue; |
| 232 | if ($current !== null) { |
| 233 | $lines[] = $current; |
| 234 | } |
| 235 | |
| 236 | $current = $word; |
| 222 | foreach (explode(' ', $line) as $word) { |
| 223 | $wordWidth = $this->markup->width($word); |
| 224 | |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 226 | $current .= ' ' . $word; |
| 227 | $currentWidth += 1 + $wordWidth; |
| 228 | |
| 229 | continue; |
| 230 | } |
| 231 | |
| 232 | if ($current !== null) { |
| 233 | $lines[] = $current; |
| 234 | } |
| 235 | |
| 236 | $current = $word; |
| 222 | foreach (explode(' ', $line) as $word) { |
| 223 | $wordWidth = $this->markup->width($word); |
| 224 | |
| 225 | if ($current !== null && ($currentWidth + 1 + $wordWidth) <= $width) { |
| 226 | $current .= ' ' . $word; |
| 227 | $currentWidth += 1 + $wordWidth; |
| 228 | |
| 229 | continue; |
| 230 | } |
| 231 | |
| 232 | if ($current !== null) { |
| 233 | $lines[] = $current; |
| 234 | } |
| 235 | |
| 236 | $current = $word; |
| 237 | $currentWidth = $wordWidth; |
| 238 | } |
| 239 | |
| 240 | $lines[] = (string) $current; |
| 241 | |
| 242 | return $lines; |
| 243 | } |
| 175 | private function write(mixed $stream, string $text): void |
| 176 | { |
| 177 | fwrite($stream, $text); |
| 178 | fflush($stream); |
| 179 | } |
| 289 | set_error_handler(static fn(): bool => true); |