From d3003ebb419ba867adfaa649bcfa3d45c03f0bf5 Mon Sep 17 00:00:00 2001
From: Dylan Baker <dylan@pnwbakers.com>
Date: Tue, 20 Apr 2021 14:16:37 -0700
Subject: [PATCH] mconf: line wrap columns nicely

I've picked 160 characters as a default because anything less than that
looks terrible and has awful wrapping going on. However, this respects
the $COLUNNS environment variable if set, and otherwise will query the
terminal to determine the size.

This is all achieved through an application of shtuil.get_terminal_size,
textwrap, print formatters, and iteration.

Fixes #6965
---
 mesonbuild/mconf.py | 54 ++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 48 insertions(+), 6 deletions(-)

diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py
index 953b3d4e4..20f31d1c7 100644
--- a/mesonbuild/mconf.py
+++ b/mesonbuild/mconf.py
@@ -12,7 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import itertools
+import shutil
 import os
+import textwrap
 import typing as T
 
 from . import build
@@ -58,6 +61,7 @@ class Conf:
         self.value_col = []
         self.choices_col = []
         self.descr_col = []
+        # XXX: is there a case where this can actually remain false?
         self.has_choices = False
         self.all_subprojects: T.Set[str] = set()
         self.yielding_options: T.Set[OptionKey] = set()
@@ -97,16 +101,54 @@ class Conf:
         # are erased when Meson is executed the next time, i.e. when
         # Ninja is run.
 
-    def print_aligned(self):
-        col_widths = (max([len(i) for i in self.name_col], default=0),
-                      max([len(i) for i in self.value_col], default=0),
-                      max([len(i) for i in self.choices_col], default=0))
+    def print_aligned(self) -> None:
+        """Do the actual printing.
+
+        This prints the generated output in an aligned, pretty form. it aims
+        for a total width of 160 characters, but will use whatever the tty
+        reports it's value to be. Though this is much wider than the standard
+        80 characters of terminals, and even than the newer 120, compressing
+        it to those lengths makes the output hard to read.
+
+        Each column will have a specific width, and will be line wrapped.
+        """
+        total_width = shutil.get_terminal_size(fallback=(160, 0))[0]
+        _col = max(total_width // 5, 20)
+        four_column = (_col, _col, _col, total_width - (3 * _col))
+        # In this case we don't have the choices field, so we can redistribute
+        # the extra 40 characters to val and desc
+        three_column = (_col, _col * 2, total_width // 2)
 
         for line in zip(self.name_col, self.value_col, self.choices_col, self.descr_col):
+            if not any(line):
+                print('')
+                continue
+
+            # This is a header, like `Subproject foo:`,
+            # We just want to print that and get on with it
+            if line[0] and not any(line[1:]):
+                print(line[0])
+                continue
+
+            # wrap will take a long string, and create a list of strings no
+            # longer than the size given. Then that list can be zipped into, to
+            # print each line of the output, such the that columns are printed
+            # to the right width, row by row.
             if self.has_choices:
-                print('{0:{width[0]}} {1:{width[1]}} {2:{width[2]}} {3}'.format(*line, width=col_widths))
+                name = textwrap.wrap(line[0], four_column[0])
+                val = textwrap.wrap(line[1], four_column[1])
+                choice = textwrap.wrap(line[2], four_column[2])
+                desc = textwrap.wrap(line[3], four_column[3])
+                for l in itertools.zip_longest(name, val, choice, desc, fillvalue=''):
+                    # We must use the length modifier here to get even rows, as
+                    # `textwrap.wrap` will only shorten, not lengthen each item
+                    print('{:{widths[0]}} {:{widths[1]}} {:{widths[2]}} {}'.format(*l, widths=four_column))
             else:
-                print('{0:{width[0]}} {1:{width[1]}} {3}'.format(*line, width=col_widths))
+                name = textwrap.wrap(line[0], three_column[0])
+                val = textwrap.wrap(line[1], three_column[1])
+                desc = textwrap.wrap(line[3], three_column[2])
+                for l in itertools.zip_longest(name, val, desc, fillvalue=''):
+                    print('{:{widths[0]}} {:{widths[1]}} {}'.format(*l, widths=three_column))
 
     def split_options_per_subproject(self, options: 'coredata.KeyedOptionDictType') -> T.Dict[str, T.Dict[str, 'UserOption']]:
         result: T.Dict[str, T.Dict[str, 'UserOption']] = {}