Columnate oversized WorkQueue UIs.

The new machines will spawn more jobs than my console has rows...

Test: ./run_tests.py
Bug: None
Change-Id: I61d1ee079488974c8d1641f5cca89c99069b045d
diff --git a/ndk/ansi.py b/ndk/ansi.py
index 379656e..3a9f6f6 100644
--- a/ndk/ansi.py
+++ b/ndk/ansi.py
@@ -19,6 +19,7 @@
 
 import contextlib
 import os
+import subprocess
 import sys
 
 try:
@@ -81,6 +82,14 @@
         return DumbConsole(stream)
 
 
+def get_console_size_linux():
+    return [int(s) for s in subprocess.check_output(['stty', 'size']).split()]
+
+
+def get_console_size_windows():
+    raise NotImplementedError
+
+
 class Console(object):
     def __init__(self, stream):
         self.stream = stream
@@ -117,6 +126,8 @@
     def __init__(self, stream):
         super(AnsiConsole, self).__init__(stream)
         self.smart_console = True
+        self._width = None
+        self._height = None
 
     def _do(self, cmd):
         print(cmd, end='', file=self.stream)
@@ -138,6 +149,24 @@
     def show_cursor(self):
         self._do(self.SHOW_CURSOR)
 
+    def init_window_size(self):
+        if os.name == 'nt':
+            self._height, self._width = get_console_size_windows()
+        else:
+            self._height, self._width = get_console_size_linux()
+
+    @property
+    def height(self):
+        if self._height is None:
+            self.init_window_size()
+        return self._height
+
+    @property
+    def width(self):
+        if self._width is None:
+            self.init_window_size()
+        return self._width
+
 
 class DumbConsole(Console):
     def __init__(self, stream):
diff --git a/ndk/test/ui.py b/ndk/test/ui.py
index c94cdeb..858ccbb 100644
--- a/ndk/test/ui.py
+++ b/ndk/test/ui.py
@@ -100,6 +100,13 @@
             for worker in self.workqueue.restricted_work_queue.workers:
                 lines.append(worker.status)
 
+        if self.ui_renderer.console.smart_console:
+            # Keep some space at the top of the UI so we can see messages.
+            ui_height = self.ui_renderer.console.height - 10
+            if ui_height > 0:
+                lines = ndk.ui.columnate(lines, self.ui_renderer.console.width,
+                                         ui_height)
+
         lines.append('{: >{width}} tests remaining'.format(
             self.workqueue.num_tasks, width=self.NUM_TESTS_DIGITS))
         return lines
diff --git a/ndk/ui.py b/ndk/ui.py
index 4d7667e..9a47b1b 100644
--- a/ndk/ui.py
+++ b/ndk/ui.py
@@ -16,7 +16,9 @@
 """UI classes for build output."""
 from __future__ import absolute_import
 from __future__ import print_function
+from __future__ import division
 
+import math
 import os
 import sys
 import time
@@ -44,9 +46,6 @@
         self.last_rendered_lines = []
         self.debug_draw = debug_draw
 
-    def get_ui_lines(self):
-        raise NotImplementedError
-
     def changed_lines(self, new_lines):
         assert len(new_lines) == len(self.last_rendered_lines)
         old_lines = self.last_rendered_lines
@@ -177,6 +176,23 @@
         ui_renderer, show_worker_status, workqueue)
 
 
+def columnate(lines, max_width, max_height):
+    if os.name == 'nt':
+        # Not yet implemented.
+        return lines
+
+    num_columns = int(math.ceil(len(lines) / max_height))
+    if num_columns == 1:
+        return lines
+
+    # Keep the columns roughly balanced.
+    num_rows = int(math.ceil(len(lines) / num_columns))
+    rows = [lines[r::num_rows] for r in range(num_rows)]
+
+    column_width = max_width // num_columns
+    return [''.join(s.ljust(column_width) for s in row) for row in rows]
+
+
 class WorkQueueUi(Ui):
     NUM_TESTS_DIGITS = 6
 
@@ -192,6 +208,13 @@
             for worker in self.workqueue.workers:
                 lines.append(worker.status)
 
+        if self.ui_renderer.console.smart_console:
+            # Keep some space at the top of the UI so we can see messages.
+            ui_height = self.ui_renderer.console.height - 10
+            if ui_height > 0:
+                lines = columnate(lines, self.ui_renderer.console.width,
+                                  ui_height)
+
         lines.append('{: >{width}} jobs remaining'.format(
             self.workqueue.num_tasks, width=self.NUM_TESTS_DIGITS))
         return lines