Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a table renderer #68

Merged
merged 9 commits into from
Sep 14, 2023
Merged

Conversation

joetannenbaum
Copy link
Contributor

This PR adds a table helper that renders a table matching the style of the rest of the package components.

I'm not sure if adding more non-interactive output is on the roadmap for the package, but I've had many use cases for this and it's nice that the output is cohesive.

It's a simple wrapper for the existing Symfony console table component:

table(
    ['Name', 'Twitter'],
    [
        ['Taylor Otwell', '@taylorotwell'],
        ['Dries Vints', '@driesvints'],
        ['James Brooks', '@jbrooksuk'],
        ['Nuno Maduro', '@enunomaduro'],
        ['Mior Muhammad Zaki', '@crynobone'],
        ['Jess Archer', '@jessarchercodes'],
        ['Guus Leeuw', '@phpguus'],
        ['Tim MacDonald', '@timacdonald87'],
        ['Joe Dixon', '@_joedixon'],
    ],
);

In context it looks like this (I played with both cyan and dim for the table headers and thought dim looked better in the end):

CleanShot 2023-09-13 at 20 43 10@2x

@jessarcher jessarcher marked this pull request as draft September 14, 2023 07:11
@jessarcher
Copy link
Member

Hey @joetannenbaum, thanks for this!

I think this is a nice addition and makes sense to provide a nice way to create tables consistent with the spacing and theme of Prompts.

There was a bug when using colspans because Symfony doesn't strip the invisible formatting characters from the border format before calculating widths. I've tweaked how the border color is applied, and it seems to work well:

Before After
image image

I've also removed the extra right padding from the last column and changed the border color from "dim" to "gray" to match the border color of the other boxes (these didn't look different with the terminal theme in your screenshot).

I've also fixed some weirdness when no headers were passed:

Before After
image image

And I've also made it so that when only the first argument is provided, they will become the rows.

@jessarcher jessarcher marked this pull request as ready for review September 14, 2023 07:55
@taylorotwell taylorotwell merged commit 85f0a1e into laravel:main Sep 14, 2023
3 checks passed
@joetannenbaum
Copy link
Contributor Author

Thanks @jessarcher! I feel like I created more work for you than solved something with this one, I'll keep an eye out for more edge cases and alternate scenarios in my next PR.

Love this package, I'll keep thinking of ways to contribute!

@jessarcher
Copy link
Member

All good @joetannenbaum! It was fun :)

I also played around with setting a minimum width on the table to match the other boxes:

image

The code is gnarly, though - I had to calculate the width of each column and then divide the remaining space evenly between them and assign any remainder to the first column. It gets especially tricky with colspans where the width gets divided amongst the columns it spans. Might stash that for now and come back to it if it stays in my head.

@joetannenbaum
Copy link
Contributor Author

Ooooooh I love the way that looks though! Great idea. Might tinker with it myself and see if I can get anywhere.

@jessarcher
Copy link
Member

Here's the code I wrote if you're interested, as I'm not going to PR it. I'm sure it could be simplified, but I'm still unsure if it's worth the complexity and potential breaking scenarios I haven't considered (like table separates, row spans, etc.)

It also has to do some weird stuff to align with how Symfony determines the col widths when there are col spans. It would be much nicer to handle this in Symfony rather than here, or at least publically expose the methods they're using so we don't need to duplicate it with potential inconsistencies and future breakages that may occur if they change things and don't consider it a breaking change because it's not part of the public API.

diff --git a/src/Themes/Default/TableRenderer.php b/src/Themes/Default/TableRenderer.php
index 185f450..623f9d0 100644
--- a/src/Themes/Default/TableRenderer.php
+++ b/src/Themes/Default/TableRenderer.php
@@ -5,6 +5,7 @@
 use Laravel\Prompts\Output\BufferedConsoleOutput;
 use Laravel\Prompts\Table;
 use Symfony\Component\Console\Helper\Table as SymfonyTable;
+use Symfony\Component\Console\Helper\TableCell;
 use Symfony\Component\Console\Helper\TableStyle;
 
 class TableRenderer extends Renderer
@@ -29,6 +30,7 @@ public function __invoke(Table $table): string
         $buffered = new BufferedConsoleOutput();
 
         (new SymfonyTable($buffered))
+            ->setColumnWidths($this->minWidths($table))
             ->setHeaders($table->headers)
             ->setRows($table->rows)
             ->setStyle($tableStyle)
@@ -39,4 +41,45 @@ public function __invoke(Table $table): string
 
         return $this;
     }
+
+    protected function minWidths(Table $table): array
+    {
+        $rows = collect([
+            ...(! is_array($table->headers[0]) ? [$table->headers] : $table->headers),
+            ...$table->rows,
+        ]);
+
+        $rowColWidths = $rows->map(fn ($row) => collect($row)->reduce(function (array $carry, $cell) {
+            return [
+                ...$carry,
+                ...match(true) {
+                    $cell instanceof TableCell && mb_strwidth($cell) => array_pad(array_map(mb_strwidth(...), mb_str_split($cell, ceil(mb_strwidth($cell) / $cell->getColspan()))), $cell->getColspan(), 0),
+                    $cell instanceof TableCell => array_fill(0, $cell->getColspan(), 0),
+                    default => [mb_strwidth($cell)],
+                },
+            ];
+        }, []));
+
+        $colWidths = array_map(max(...), ...$rowColWidths);
+
+        $colCount = count($colWidths);
+
+        $overallWidth = array_sum($colWidths) + ($colCount * 3) + 2;
+
+        $minWidth = min(65, $table->terminal()->cols() - 1);
+
+        $spaceRemaining = $minWidth - $overallWidth;
+
+        if ($spaceRemaining < 1) {
+            return $colWidths;
+        }
+
+        $divided = floor($spaceRemaining / $colCount);
+        $remainder = $spaceRemaining % $colCount;
+
+        $minWidths = collect($colWidths)->map(fn ($width) => $width + $divided)->all();
+        $minWidths[0] += $remainder;
+
+        return $minWidths;
+    }
 }

@jessarcher
Copy link
Member

Oh and speaking of Symfony, if you're interested in trying a PR there, I think it would make sense if they used their Helper::removeDecoration method before calculating the column separator width in the Table::getColumnSeparatorWidth method, as they do in a few other places.

That would allow us to use your original approach for changing the border color which was a lot nicer. We'd need to increase the minimum required Symfony version though, which probably isn't ideal - at least for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants