From 3e353855cbfaf04c793c3c86e15c59bb85d2b020 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:28:19 +0000 Subject: [PATCH 1/5] Initial plan From 01d65c76ad167138864af4fc7615ffabbec1ff2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:32:29 +0000 Subject: [PATCH 2/5] Add wrapping mode configuration for table columns - Add setWrappingMode() method to Ascii renderer and Table class - Support three modes: 'wrap' (default), 'word-wrap', and 'truncate' - word-wrap mode wraps at word boundaries (spaces/hyphens) - truncate mode truncates with ellipsis (...) - Add helper methods wrapText() and wordWrap() for wrapping logic - Add tests for new functionality Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- examples/table-wrapping.php | 42 ++++++++++ lib/cli/Table.php | 13 +++ lib/cli/table/Ascii.php | 153 ++++++++++++++++++++++++++++++++---- tests/Test_Table_Ascii.php | 73 +++++++++++++++++ 4 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 examples/table-wrapping.php diff --git a/examples/table-wrapping.php b/examples/table-wrapping.php new file mode 100644 index 0000000..5016517 --- /dev/null +++ b/examples/table-wrapping.php @@ -0,0 +1,42 @@ +setHeaders($headers); +$table->setRows($data); +$renderer = new \cli\table\Ascii(); +$renderer->setConstraintWidth(70); // Simulate narrower terminal +$table->setRenderer($renderer); +$table->display(); + +echo "\n=== Word-wrap mode (wrap at word boundaries) ===\n"; +$table = new \cli\Table(); +$table->setHeaders($headers); +$table->setRows($data); +$renderer = new \cli\table\Ascii(); +$renderer->setConstraintWidth(70); // Simulate narrower terminal +$table->setRenderer($renderer); +$table->setWrappingMode('word-wrap'); +$table->display(); + +echo "\n=== Truncate mode (truncate with ellipsis) ===\n"; +$table = new \cli\Table(); +$table->setHeaders($headers); +$table->setRows($data); +$renderer = new \cli\table\Ascii(); +$renderer->setConstraintWidth(70); // Simulate narrower terminal +$table->setRenderer($renderer); +$table->setWrappingMode('truncate'); +$table->display(); diff --git a/lib/cli/Table.php b/lib/cli/Table.php index d79c16f..8ae90aa 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -320,6 +320,19 @@ public function setAsciiPreColorized( $pre_colorized ) { } } + /** + * Set the wrapping mode for table cells. + * + * @param string $mode One of: 'wrap' (default - wrap at character boundaries), + * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis). + * @see cli\Ascii::setWrappingMode() + */ + public function setWrappingMode( $mode ) { + if ( $this->_renderer instanceof Ascii ) { + $this->_renderer->setWrappingMode( $mode ); + } + } + /** * Is a column in an Ascii table pre-colorized? * diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 64d1a89..20ef0fa 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -28,6 +28,7 @@ class Ascii extends Renderer { protected $_border = null; protected $_constraintWidth = null; protected $_pre_colorized = false; + protected $_wrapping_mode = 'wrap'; // 'wrap', 'word-wrap', or 'truncate' /** * Set the widths of each column in the table. @@ -96,6 +97,20 @@ public function setConstraintWidth( $constraintWidth ) { $this->_constraintWidth = $constraintWidth; } + /** + * Set the wrapping mode for table cells. + * + * @param string $mode One of: 'wrap' (default - wrap at character boundaries), + * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis). + */ + public function setWrappingMode( $mode ) { + $valid_modes = array( 'wrap', 'word-wrap', 'truncate' ); + if ( ! in_array( $mode, $valid_modes, true ) ) { + throw new \InvalidArgumentException( "Invalid wrapping mode '$mode'. Must be one of: " . implode( ', ', $valid_modes ) ); + } + $this->_wrapping_mode = $mode; + } + /** * Set the characters used for rendering the Ascii table. * @@ -148,21 +163,8 @@ public function row( array $row ) { $wrapped_lines = []; foreach ( $split_lines as $line ) { - // Use the new color-aware wrapping for pre-colorized content - if ( self::isPreColorized( $col ) && Colors::width( $line, true, $encoding ) > $col_width ) { - $line_wrapped = Colors::wrapPreColorized( $line, $col_width, $encoding ); - $wrapped_lines = array_merge( $wrapped_lines, $line_wrapped ); - } else { - // For non-colorized content, use the original logic - do { - $wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding ); - $val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding ); - if ( $val_width ) { - $wrapped_lines[] = $wrapped_value; - $line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); - } - } while ( $line ); - } + $line_wrapped = $this->wrapText( $line, $col_width, $encoding, self::isPreColorized( $col ) ); + $wrapped_lines = array_merge( $wrapped_lines, $line_wrapped ); } $row[ $col ] = array_shift( $wrapped_lines ); @@ -235,6 +237,127 @@ public function setPreColorized( $pre_colorized ) { $this->_pre_colorized = $pre_colorized; } + /** + * Wrap text based on the configured wrapping mode. + * + * @param string $text The text to wrap. + * @param int $width The maximum width. + * @param string|bool $encoding The text encoding. + * @param bool $is_precolorized Whether the text is pre-colorized. + * @return array Array of wrapped lines. + */ + protected function wrapText( $text, $width, $encoding, $is_precolorized ) { + if ( ! $width ) { + return array( $text ); + } + + $text_width = Colors::width( $text, $is_precolorized, $encoding ); + + // If text fits, no wrapping needed + if ( $text_width <= $width ) { + return array( $text ); + } + + // Handle truncate mode + if ( 'truncate' === $this->_wrapping_mode ) { + $ellipsis = '...'; + $ellipsis_width = 3; + + if ( $width <= $ellipsis_width ) { + // Not enough space for ellipsis, just truncate + return array( \cli\safe_substr( $text, 0, $width, true /*is_width*/, $encoding ) ); + } + + // Truncate and add ellipsis + $truncated = \cli\safe_substr( $text, 0, $width - $ellipsis_width, true /*is_width*/, $encoding ); + return array( $truncated . $ellipsis ); + } + + // Handle word-wrap mode + if ( 'word-wrap' === $this->_wrapping_mode ) { + return $this->wordWrap( $text, $width, $encoding, $is_precolorized ); + } + + // Default: character-boundary wrapping + $wrapped_lines = array(); + $line = $text; + + // Use the new color-aware wrapping for pre-colorized content + if ( $is_precolorized && Colors::width( $line, true, $encoding ) > $width ) { + $wrapped_lines = Colors::wrapPreColorized( $line, $width, $encoding ); + } else { + // For non-colorized content, use character-boundary wrapping + do { + $wrapped_value = \cli\safe_substr( $line, 0, $width, true /*is_width*/, $encoding ); + $val_width = Colors::width( $wrapped_value, $is_precolorized, $encoding ); + if ( $val_width ) { + $wrapped_lines[] = $wrapped_value; + $line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + } + } while ( $line ); + } + + return $wrapped_lines; + } + + /** + * Wrap text at word boundaries. + * + * @param string $text The text to wrap. + * @param int $width The maximum width. + * @param string|bool $encoding The text encoding. + * @param bool $is_precolorized Whether the text is pre-colorized. + * @return array Array of wrapped lines. + */ + protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { + $wrapped_lines = array(); + $current_line = ''; + + // Split by spaces and hyphens while keeping the delimiters + $words = preg_split( '/(\s+|-)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + + foreach ( $words as $word ) { + $word_width = Colors::width( $word, $is_precolorized, $encoding ); + $current_line_width = Colors::width( $current_line, $is_precolorized, $encoding ); + + // If this word alone exceeds the width, we need to split it + if ( $word_width > $width ) { + // Flush current line if not empty + if ( $current_line !== '' ) { + $wrapped_lines[] = $current_line; + $current_line = ''; + $current_line_width = 0; + } + + // Split the long word at character boundaries + $remaining_word = $word; + while ( $remaining_word ) { + $chunk = \cli\safe_substr( $remaining_word, 0, $width, true /*is_width*/, $encoding ); + $wrapped_lines[] = $chunk; + $remaining_word = \cli\safe_substr( $remaining_word, \cli\safe_strlen( $chunk, $encoding ), null /*length*/, false /*is_width*/, $encoding ); + } + continue; + } + + // Check if adding this word would exceed the width + if ( $current_line !== '' && $current_line_width + $word_width > $width ) { + // Start a new line + $wrapped_lines[] = $current_line; + $current_line = $word; + } else { + // Add to current line + $current_line .= $word; + } + } + + // Add any remaining content + if ( $current_line !== '' ) { + $wrapped_lines[] = $current_line; + } + + return $wrapped_lines ?: array( '' ); + } + /** * Is a column pre-colorized? * diff --git a/tests/Test_Table_Ascii.php b/tests/Test_Table_Ascii.php index 263dfb3..3e683b8 100644 --- a/tests/Test_Table_Ascii.php +++ b/tests/Test_Table_Ascii.php @@ -153,6 +153,79 @@ public function testWrappedColorizedText() { $this->assertOutFileEqualsWith($output); } + /** + * Test word-wrapping mode keeps words together. + */ + public function testWordWrappingMode() { + $headers = array('name', 'status'); + $rows = array( + array('all-in-one-wp-migration-multisite-extension', 'inactive'), + ); + + // With word-wrap, the hyphenated words should wrap at hyphens + $output = <<<'OUT' ++----------------------+----------+ +| name | status | ++----------------------+----------+ +| all-in-one-wp- | inactive | +| migration-multisite- | | +| extension | | ++----------------------+----------+ + +OUT; + + $this->_instance->setHeaders($headers); + $this->_instance->setRows($rows); + $renderer = new Ascii([20, 8]); + $renderer->setConstraintWidth(36); + $this->_instance->setRenderer($renderer); + $this->_instance->setWrappingMode('word-wrap'); + $this->_instance->display(); + $this->assertOutFileEqualsWith($output); + } + + /** + * Test truncate mode with ellipsis. + */ + public function testTruncateMode() { + $headers = array('name', 'status'); + $rows = array( + array('all-in-one-wp-migration-multisite-extension', 'inactive'), + array('short', 'active'), + ); + + // With truncate, long names should be truncated with ellipsis + $output = <<<'OUT' ++----------------------+----------+ +| name | status | ++----------------------+----------+ +| all-in-one-wp-mig... | inactive | +| short | active | ++----------------------+----------+ + +OUT; + + $this->_instance->setHeaders($headers); + $this->_instance->setRows($rows); + $renderer = new Ascii([20, 8]); + $renderer->setConstraintWidth(36); + $this->_instance->setRenderer($renderer); + $this->_instance->setWrappingMode('truncate'); + $this->_instance->display(); + $this->assertOutFileEqualsWith($output); + } + + /** + * Test that wrapping mode setter validates input. + */ + public function testWrappingModeValidation() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid wrapping mode 'invalid'"); + + $renderer = new Ascii(); + $renderer->setWrappingMode('invalid'); + } + /** * Checks that spacing and borders are handled correctly in table */ From a28b281767054421f60457ebf85a9999b9f71cd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:33:16 +0000 Subject: [PATCH 3/5] Update table wrapping example with documentation - Add comprehensive examples for all three wrapping modes - Include explanations of when to use each mode - Add usage instructions in the example output - Make executable with proper shebang Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- examples/table-wrapping.php | 55 ++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/examples/table-wrapping.php b/examples/table-wrapping.php index 5016517..4c91976 100644 --- a/examples/table-wrapping.php +++ b/examples/table-wrapping.php @@ -1,6 +1,23 @@ +#!/usr/bin/env php setHeaders($headers); $table->setRows($data); @@ -20,8 +41,13 @@ $renderer->setConstraintWidth(70); // Simulate narrower terminal $table->setRenderer($renderer); $table->display(); +cli\line(); -echo "\n=== Word-wrap mode (wrap at word boundaries) ===\n"; +// Example 2: Word-wrap mode (wrap at word boundaries) +cli\line('%Y## Example 2: Word-Wrap Mode (Wrap at Word Boundaries)%n'); +cli\line('Word-wrap mode keeps words together by wrapping at spaces and hyphens.'); +cli\line('This makes it easier to read and copy/paste long values.'); +cli\line(); $table = new \cli\Table(); $table->setHeaders($headers); $table->setRows($data); @@ -30,8 +56,13 @@ $table->setRenderer($renderer); $table->setWrappingMode('word-wrap'); $table->display(); +cli\line(); -echo "\n=== Truncate mode (truncate with ellipsis) ===\n"; +// Example 3: Truncate mode (truncate with ellipsis) +cli\line('%Y## Example 3: Truncate Mode (Truncate with Ellipsis)%n'); +cli\line('Truncate mode cuts off long content and adds "..." to indicate truncation.'); +cli\line('This is useful when you want a compact display and don\'t need full values.'); +cli\line(); $table = new \cli\Table(); $table->setHeaders($headers); $table->setRows($data); @@ -40,3 +71,19 @@ $table->setRenderer($renderer); $table->setWrappingMode('truncate'); $table->display(); +cli\line(); + +// Example 4: Usage instructions +cli\line('%Y## Wrapping Mode Options%n'); +cli\line(); +cli\line('You can use the following wrapping modes:'); +cli\line(' %G*%n %Cwrap%n - Default: wrap at character boundaries'); +cli\line(' %G*%n %Cword-wrap%n - Wrap at word boundaries (spaces/hyphens)'); +cli\line(' %G*%n %Ctruncate%n - Truncate with ellipsis (...)'); +cli\line(); +cli\line('Example usage:'); +cli\line(' %c$table->setWrappingMode(\'word-wrap\');%n'); +cli\line(); + +cli\line('%GDone!%n'); +cli\line(); From 867bc04794e28ebc55ec947804cc60f3c958ac62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:52:27 +0000 Subject: [PATCH 4/5] Refactor: Use class constant for valid wrapping modes and optimize width tracking - Define VALID_WRAPPING_MODES as a private class constant for better maintainability - Optimize wordWrap() by tracking width incrementally instead of recalculating Colors::width() on every iteration - Addresses code review feedback from @swissspidy Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- lib/cli/table/Ascii.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 20ef0fa..c18f1eb 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -19,6 +19,11 @@ * The ASCII renderer renders tables with ASCII borders. */ class Ascii extends Renderer { + /** + * Valid wrapping modes. + */ + private const VALID_WRAPPING_MODES = array( 'wrap', 'word-wrap', 'truncate' ); + protected $_characters = array( 'corner' => '+', 'line' => '-', @@ -104,9 +109,8 @@ public function setConstraintWidth( $constraintWidth ) { * 'word-wrap' (wrap at word boundaries), or 'truncate' (truncate with ellipsis). */ public function setWrappingMode( $mode ) { - $valid_modes = array( 'wrap', 'word-wrap', 'truncate' ); - if ( ! in_array( $mode, $valid_modes, true ) ) { - throw new \InvalidArgumentException( "Invalid wrapping mode '$mode'. Must be one of: " . implode( ', ', $valid_modes ) ); + if ( ! in_array( $mode, self::VALID_WRAPPING_MODES, true ) ) { + throw new \InvalidArgumentException( "Invalid wrapping mode '$mode'. Must be one of: " . implode( ', ', self::VALID_WRAPPING_MODES ) ); } $this->_wrapping_mode = $mode; } @@ -312,13 +316,13 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { $wrapped_lines = array(); $current_line = ''; + $current_line_width = 0; // Split by spaces and hyphens while keeping the delimiters $words = preg_split( '/(\s+|-)/u', $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); foreach ( $words as $word ) { $word_width = Colors::width( $word, $is_precolorized, $encoding ); - $current_line_width = Colors::width( $current_line, $is_precolorized, $encoding ); // If this word alone exceeds the width, we need to split it if ( $word_width > $width ) { @@ -344,9 +348,11 @@ protected function wordWrap( $text, $width, $encoding, $is_precolorized ) { // Start a new line $wrapped_lines[] = $current_line; $current_line = $word; + $current_line_width = $word_width; } else { // Add to current line $current_line .= $word; + $current_line_width += $word_width; } } From 49350207b0599133ed07e5ab5f88232bb3d20e09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:57:58 +0000 Subject: [PATCH 5/5] Refactor: Add ellipsis constants and simplify pre-colorized check - Define ELLIPSIS and ELLIPSIS_WIDTH as class constants for better maintainability - Remove redundant width check in pre-colorized condition (already validated earlier) - Addresses code review feedback from @swissspidy Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- lib/cli/table/Ascii.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index c18f1eb..fd20bc9 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -24,6 +24,16 @@ class Ascii extends Renderer { */ private const VALID_WRAPPING_MODES = array( 'wrap', 'word-wrap', 'truncate' ); + /** + * Ellipsis character(s) used for truncation. + */ + private const ELLIPSIS = '...'; + + /** + * Width of the ellipsis in characters. + */ + private const ELLIPSIS_WIDTH = 3; + protected $_characters = array( 'corner' => '+', 'line' => '-', @@ -264,17 +274,14 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { // Handle truncate mode if ( 'truncate' === $this->_wrapping_mode ) { - $ellipsis = '...'; - $ellipsis_width = 3; - - if ( $width <= $ellipsis_width ) { + if ( $width <= self::ELLIPSIS_WIDTH ) { // Not enough space for ellipsis, just truncate return array( \cli\safe_substr( $text, 0, $width, true /*is_width*/, $encoding ) ); } // Truncate and add ellipsis - $truncated = \cli\safe_substr( $text, 0, $width - $ellipsis_width, true /*is_width*/, $encoding ); - return array( $truncated . $ellipsis ); + $truncated = \cli\safe_substr( $text, 0, $width - self::ELLIPSIS_WIDTH, true /*is_width*/, $encoding ); + return array( $truncated . self::ELLIPSIS ); } // Handle word-wrap mode @@ -287,7 +294,7 @@ protected function wrapText( $text, $width, $encoding, $is_precolorized ) { $line = $text; // Use the new color-aware wrapping for pre-colorized content - if ( $is_precolorized && Colors::width( $line, true, $encoding ) > $width ) { + if ( $is_precolorized ) { $wrapped_lines = Colors::wrapPreColorized( $line, $width, $encoding ); } else { // For non-colorized content, use character-boundary wrapping