From b694d8b290dfa6cdd024cfaeac983060e0fa882a Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 29 Oct 2026 17:01:50 +0800 Subject: [PATCH] feat: add `Output::wrap()` --- system/CLI/CLI.php | 27 +---------- system/CLI/Output.php | 12 +++++ system/CLI/Prompt.php | 5 +- tests/system/CLI/OutputTest.php | 53 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 8 ++-- user_guide_src/source/cli/cli_library.rst | 4 ++ 6 files changed, 75 insertions(+), 34 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 1ed1ecb21efe..0b461f176512 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -520,32 +520,7 @@ public static function showProgress($thisStep = 1, int $totalSteps = 10) */ public static function wrap(?string $string = null, int $max = 0, int $padLeft = 0): string { - if ((string) $string === '') { - return ''; - } - - $width = static::getWidth(); - $max = ($max === 0 ? $width : min($max, $width)) - $padLeft; - - $lines = wordwrap($string, $max, PHP_EOL); - - if ($padLeft > 0) { - $lines = explode(PHP_EOL, $lines); - - $first = true; - - array_walk($lines, static function (&$line) use ($padLeft, &$first): void { - if (! $first) { - $line = str_repeat(' ', $padLeft) . $line; - } else { - $first = false; - } - }); - - $lines = implode(PHP_EOL, $lines); - } - - return $lines; + return static::getOutput()->wrap((string) $string, $max, $padLeft); } // -------------------------------------------------------------------- diff --git a/system/CLI/Output.php b/system/CLI/Output.php index 1b4fae782f3b..3a8a31a92f3c 100644 --- a/system/CLI/Output.php +++ b/system/CLI/Output.php @@ -185,6 +185,18 @@ public function color(string $text, string $foreground, string $background = '', return $this->isDecorated() ? Color::apply($text, $foreground, $background, $format) : $text; } + /** + * Returns the text wrapped at the terminal width, or at `$max` when narrower, + * with every line after the first indented by `$padLeft` spaces. + */ + public function wrap(string $text, int $max = 0, int $padLeft = 0): string + { + $width = $this->terminal->getWidth(); + $max = ($max === 0 ? $width : min($max, $width)) - $padLeft; + + return implode(PHP_EOL . str_repeat(' ', $padLeft), explode(PHP_EOL, wordwrap($text, $max, PHP_EOL))); + } + /** * Whether the target stream supports colours. */ diff --git a/system/CLI/Prompt.php b/system/CLI/Prompt.php index 67d1e0f4d705..4a2442405d4f 100644 --- a/system/CLI/Prompt.php +++ b/system/CLI/Prompt.php @@ -594,15 +594,12 @@ private function interrupt(): void private function writeKeysAndValues(array $options): void { $padLeft = max(array_map(mb_strwidth(...), array_keys($options))) + 6; - $width = min(self::MAX_LINE_WIDTH, $this->output->getTerminal()->getWidth()) - $padLeft; foreach ($options as $key => $description) { - $lines = explode(PHP_EOL, wordwrap($description, $width, PHP_EOL)); - $this->output->write(sprintf( '%s%s', $this->output->color(str_pad(sprintf(' [%s] ', $key), $padLeft), 'green'), - implode(sprintf('%s%s', PHP_EOL, str_repeat(' ', $padLeft)), $lines), + $this->output->wrap($description, self::MAX_LINE_WIDTH, $padLeft), )); } } diff --git a/tests/system/CLI/OutputTest.php b/tests/system/CLI/OutputTest.php index ea471c87b8f5..bb39b26de869 100644 --- a/tests/system/CLI/OutputTest.php +++ b/tests/system/CLI/OutputTest.php @@ -227,6 +227,51 @@ public function testColorRejectsAnUnknownBackground(): void $output->color('test', 'white', 'Background'); } + public function testWrapBreaksAtTheTerminalWidth(): void + { + $output = new Output(new RecordingInputOutput(), $this->createTerminalOfWidth(20)); + + $expected = <<<'EOT' + the quick brown fox + jumps over the lazy + dog + EOT; + + $this->assertSame($expected, $output->wrap('the quick brown fox jumps over the lazy dog')); + $this->assertSame($expected, $output->wrap('the quick brown fox jumps over the lazy dog', 999)); + } + + public function testWrapBreaksAtANarrowerMax(): void + { + $output = new Output(new RecordingInputOutput(), $this->createTerminalOfWidth(20)); + + $this->assertSame(<<<'EOT' + the quick + brown fox + jumps over + the lazy + dog + EOT, $output->wrap('the quick brown fox jumps over the lazy dog', 10)); + } + + public function testWrapIndentsTheLinesAfterTheFirst(): void + { + $output = new Output(new RecordingInputOutput(), $this->createTerminalOfWidth(20)); + + $this->assertSame(<<<'EOT' + the quick brown + fox jumps over + the lazy dog + EOT, $output->wrap('the quick brown fox jumps over the lazy dog', 0, 4)); + } + + public function testWrapLeavesAnEmptyTextEmpty(): void + { + $output = new Output(new RecordingInputOutput(), $this->createTerminalOfWidth(20)); + + $this->assertSame('', $output->wrap('', 0, 4)); + } + public function testBeepRepeats(): void { $io = new RecordingInputOutput(); @@ -663,4 +708,12 @@ private function getWrittenText(RecordingInputOutput $io): string { return implode('', array_column($io->writes, 1)); } + + private function createTerminalOfWidth(int $width): Terminal + { + $terminal = $this->createStub(Terminal::class); + $terminal->method('getWidth')->willReturn($width); + + return $terminal; + } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index d619828a2d75..dbbe8f8281ea 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -270,10 +270,10 @@ Commands - Added ``key:rotate`` command to demote the current ``encryption.key`` to ``encryption.previousKeys`` in **.env** and generate a new key. See :ref:`spark-key-rotate`. - Added ``AbstractCommand::callSilently()`` to invoke another command with its output discarded, restoring the prior output afterwards. See :ref:`modern-commands-call-silently`. - The output side of the ``CLI`` class now lives in instance classes, with ``CLI`` delegating to them: :php:class:`Output ` - (``print()``, ``write()``, ``error()``, ``newLine()``, ``deferNewLine()``, ``beep()``, ``clearScreen()``, ``color()``), :php:class:`Terminal ` - (dimensions and colour support), :php:class:`ProgressBar `, :php:class:`Table `, and - :php:class:`Color ` (ANSI codes and ``Color::width()``). ``CLI::getOutput()`` and ``CLI::getTerminal()`` return the shared - instances. See :ref:`cli-library-classes`. + (``print()``, ``write()``, ``error()``, ``newLine()``, ``deferNewLine()``, ``beep()``, ``clearScreen()``, ``color()``, ``wrap()``), + :php:class:`Terminal ` (dimensions and colour support), :php:class:`ProgressBar `, + :php:class:`Table `, and :php:class:`Color ` (ANSI codes and ``Color::width()``). + ``CLI::getOutput()`` and ``CLI::getTerminal()`` return the shared instances. See :ref:`cli-library-classes`. - Modern commands can write through ``$this->getOutput()``, the same ``Output`` used by the static ``CLI`` methods. ``AbstractCommand::run()`` and ``Commands::runCommand()`` accept an ``Output`` to run against, which ``Console`` passes down and sub-commands inherit. See :ref:`modern-commands-writing-output`. diff --git a/user_guide_src/source/cli/cli_library.rst b/user_guide_src/source/cli/cli_library.rst index 357f32e457f0..13d5aa2e70a0 100644 --- a/user_guide_src/source/cli/cli_library.rst +++ b/user_guide_src/source/cli/cli_library.rst @@ -372,6 +372,10 @@ output of their run through ``$this->getPrompt()``: ``print()``, ``write()``, and ``error()`` colour through this method, so a subclass overriding it changes the colouring of every write. + .. php:method:: wrap(string $text[, int $max = 0[, int $padLeft = 0]]): string + + The same as ``CLI::wrap()``, measuring the terminal of this output. + .. php:method:: isDecorated(): bool Whether the target stream supports colours.