| from collections import defaultdict |
| from itertools import chain |
| from operator import itemgetter |
| from typing import Dict, Iterable, List, Optional, Tuple |
| |
| from .align import Align, AlignMethod |
| from .console import Console, ConsoleOptions, RenderableType, RenderResult |
| from .constrain import Constrain |
| from .measure import Measurement |
| from .padding import Padding, PaddingDimensions |
| from .table import Table |
| from .text import TextType |
| from .jupyter import JupyterMixin |
| |
| |
| class Columns(JupyterMixin): |
| """Display renderables in neat columns. |
| |
| Args: |
| renderables (Iterable[RenderableType]): Any number of Rich renderables (including str). |
| width (int, optional): The desired width of the columns, or None to auto detect. Defaults to None. |
| padding (PaddingDimensions, optional): Optional padding around cells. Defaults to (0, 1). |
| expand (bool, optional): Expand columns to full width. Defaults to False. |
| equal (bool, optional): Arrange in to equal sized columns. Defaults to False. |
| column_first (bool, optional): Align items from top to bottom (rather than left to right). Defaults to False. |
| right_to_left (bool, optional): Start column from right hand side. Defaults to False. |
| align (str, optional): Align value ("left", "right", or "center") or None for default. Defaults to None. |
| title (TextType, optional): Optional title for Columns. |
| """ |
| |
| def __init__( |
| self, |
| renderables: Optional[Iterable[RenderableType]] = None, |
| padding: PaddingDimensions = (0, 1), |
| *, |
| width: Optional[int] = None, |
| expand: bool = False, |
| equal: bool = False, |
| column_first: bool = False, |
| right_to_left: bool = False, |
| align: Optional[AlignMethod] = None, |
| title: Optional[TextType] = None, |
| ) -> None: |
| self.renderables = list(renderables or []) |
| self.width = width |
| self.padding = padding |
| self.expand = expand |
| self.equal = equal |
| self.column_first = column_first |
| self.right_to_left = right_to_left |
| self.align: Optional[AlignMethod] = align |
| self.title = title |
| |
| def add_renderable(self, renderable: RenderableType) -> None: |
| """Add a renderable to the columns. |
| |
| Args: |
| renderable (RenderableType): Any renderable object. |
| """ |
| self.renderables.append(renderable) |
| |
| def __rich_console__( |
| self, console: Console, options: ConsoleOptions |
| ) -> RenderResult: |
| render_str = console.render_str |
| renderables = [ |
| render_str(renderable) if isinstance(renderable, str) else renderable |
| for renderable in self.renderables |
| ] |
| if not renderables: |
| return |
| _top, right, _bottom, left = Padding.unpack(self.padding) |
| width_padding = max(left, right) |
| max_width = options.max_width |
| widths: Dict[int, int] = defaultdict(int) |
| column_count = len(renderables) |
| |
| get_measurement = Measurement.get |
| renderable_widths = [ |
| get_measurement(console, options, renderable).maximum |
| for renderable in renderables |
| ] |
| if self.equal: |
| renderable_widths = [max(renderable_widths)] * len(renderable_widths) |
| |
| def iter_renderables( |
| column_count: int, |
| ) -> Iterable[Tuple[int, Optional[RenderableType]]]: |
| item_count = len(renderables) |
| if self.column_first: |
| width_renderables = list(zip(renderable_widths, renderables)) |
| |
| column_lengths: List[int] = [item_count // column_count] * column_count |
| for col_no in range(item_count % column_count): |
| column_lengths[col_no] += 1 |
| |
| row_count = (item_count + column_count - 1) // column_count |
| cells = [[-1] * column_count for _ in range(row_count)] |
| row = col = 0 |
| for index in range(item_count): |
| cells[row][col] = index |
| column_lengths[col] -= 1 |
| if column_lengths[col]: |
| row += 1 |
| else: |
| col += 1 |
| row = 0 |
| for index in chain.from_iterable(cells): |
| if index == -1: |
| break |
| yield width_renderables[index] |
| else: |
| yield from zip(renderable_widths, renderables) |
| # Pad odd elements with spaces |
| if item_count % column_count: |
| for _ in range(column_count - (item_count % column_count)): |
| yield 0, None |
| |
| table = Table.grid(padding=self.padding, collapse_padding=True, pad_edge=False) |
| table.expand = self.expand |
| table.title = self.title |
| |
| if self.width is not None: |
| column_count = (max_width) // (self.width + width_padding) |
| for _ in range(column_count): |
| table.add_column(width=self.width) |
| else: |
| while column_count > 1: |
| widths.clear() |
| column_no = 0 |
| for renderable_width, _ in iter_renderables(column_count): |
| widths[column_no] = max(widths[column_no], renderable_width) |
| total_width = sum(widths.values()) + width_padding * ( |
| len(widths) - 1 |
| ) |
| if total_width > max_width: |
| column_count = len(widths) - 1 |
| break |
| else: |
| column_no = (column_no + 1) % column_count |
| else: |
| break |
| |
| get_renderable = itemgetter(1) |
| _renderables = [ |
| get_renderable(_renderable) |
| for _renderable in iter_renderables(column_count) |
| ] |
| if self.equal: |
| _renderables = [ |
| None |
| if renderable is None |
| else Constrain(renderable, renderable_widths[0]) |
| for renderable in _renderables |
| ] |
| if self.align: |
| align = self.align |
| _Align = Align |
| _renderables = [ |
| None if renderable is None else _Align(renderable, align) |
| for renderable in _renderables |
| ] |
| |
| right_to_left = self.right_to_left |
| add_row = table.add_row |
| for start in range(0, len(_renderables), column_count): |
| row = _renderables[start : start + column_count] |
| if right_to_left: |
| row = row[::-1] |
| add_row(*row) |
| yield table |
| |
| |
| if __name__ == "__main__": # pragma: no cover |
| import os |
| |
| console = Console() |
| |
| files = [f"{i} {s}" for i, s in enumerate(sorted(os.listdir()))] |
| columns = Columns(files, padding=(0, 1), expand=False, equal=False) |
| console.print(columns) |
| console.rule() |
| columns.column_first = True |
| console.print(columns) |
| columns.right_to_left = True |
| console.rule() |
| console.print(columns) |