| | import sys |
| | from itertools import chain |
| | from typing import TYPE_CHECKING, Iterable, Optional |
| |
|
| | if sys.version_info >= (3, 8): |
| | from typing import Literal |
| | else: |
| | from pip._vendor.typing_extensions import Literal |
| |
|
| | from .constrain import Constrain |
| | from .jupyter import JupyterMixin |
| | from .measure import Measurement |
| | from .segment import Segment |
| | from .style import StyleType |
| |
|
| | if TYPE_CHECKING: |
| | from .console import Console, ConsoleOptions, RenderableType, RenderResult |
| |
|
| | AlignMethod = Literal["left", "center", "right"] |
| | VerticalAlignMethod = Literal["top", "middle", "bottom"] |
| |
|
| |
|
| | class Align(JupyterMixin): |
| | """Align a renderable by adding spaces if necessary. |
| | |
| | Args: |
| | renderable (RenderableType): A console renderable. |
| | align (AlignMethod): One of "left", "center", or "right"" |
| | style (StyleType, optional): An optional style to apply to the background. |
| | vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None. |
| | pad (bool, optional): Pad the right with spaces. Defaults to True. |
| | width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None. |
| | height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None. |
| | |
| | Raises: |
| | ValueError: if ``align`` is not one of the expected values. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | renderable: "RenderableType", |
| | align: AlignMethod = "left", |
| | style: Optional[StyleType] = None, |
| | *, |
| | vertical: Optional[VerticalAlignMethod] = None, |
| | pad: bool = True, |
| | width: Optional[int] = None, |
| | height: Optional[int] = None, |
| | ) -> None: |
| | if align not in ("left", "center", "right"): |
| | raise ValueError( |
| | f'invalid value for align, expected "left", "center", or "right" (not {align!r})' |
| | ) |
| | if vertical is not None and vertical not in ("top", "middle", "bottom"): |
| | raise ValueError( |
| | f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})' |
| | ) |
| | self.renderable = renderable |
| | self.align = align |
| | self.style = style |
| | self.vertical = vertical |
| | self.pad = pad |
| | self.width = width |
| | self.height = height |
| |
|
| | def __repr__(self) -> str: |
| | return f"Align({self.renderable!r}, {self.align!r})" |
| |
|
| | @classmethod |
| | def left( |
| | cls, |
| | renderable: "RenderableType", |
| | style: Optional[StyleType] = None, |
| | *, |
| | vertical: Optional[VerticalAlignMethod] = None, |
| | pad: bool = True, |
| | width: Optional[int] = None, |
| | height: Optional[int] = None, |
| | ) -> "Align": |
| | """Align a renderable to the left.""" |
| | return cls( |
| | renderable, |
| | "left", |
| | style=style, |
| | vertical=vertical, |
| | pad=pad, |
| | width=width, |
| | height=height, |
| | ) |
| |
|
| | @classmethod |
| | def center( |
| | cls, |
| | renderable: "RenderableType", |
| | style: Optional[StyleType] = None, |
| | *, |
| | vertical: Optional[VerticalAlignMethod] = None, |
| | pad: bool = True, |
| | width: Optional[int] = None, |
| | height: Optional[int] = None, |
| | ) -> "Align": |
| | """Align a renderable to the center.""" |
| | return cls( |
| | renderable, |
| | "center", |
| | style=style, |
| | vertical=vertical, |
| | pad=pad, |
| | width=width, |
| | height=height, |
| | ) |
| |
|
| | @classmethod |
| | def right( |
| | cls, |
| | renderable: "RenderableType", |
| | style: Optional[StyleType] = None, |
| | *, |
| | vertical: Optional[VerticalAlignMethod] = None, |
| | pad: bool = True, |
| | width: Optional[int] = None, |
| | height: Optional[int] = None, |
| | ) -> "Align": |
| | """Align a renderable to the right.""" |
| | return cls( |
| | renderable, |
| | "right", |
| | style=style, |
| | vertical=vertical, |
| | pad=pad, |
| | width=width, |
| | height=height, |
| | ) |
| |
|
| | def __rich_console__( |
| | self, console: "Console", options: "ConsoleOptions" |
| | ) -> "RenderResult": |
| | align = self.align |
| | width = console.measure(self.renderable, options=options).maximum |
| | rendered = console.render( |
| | Constrain( |
| | self.renderable, width if self.width is None else min(width, self.width) |
| | ), |
| | options.update(height=None), |
| | ) |
| | lines = list(Segment.split_lines(rendered)) |
| | width, height = Segment.get_shape(lines) |
| | lines = Segment.set_shape(lines, width, height) |
| | new_line = Segment.line() |
| | excess_space = options.max_width - width |
| | style = console.get_style(self.style) if self.style is not None else None |
| |
|
| | def generate_segments() -> Iterable[Segment]: |
| | if excess_space <= 0: |
| | |
| | for line in lines: |
| | yield from line |
| | yield new_line |
| |
|
| | elif align == "left": |
| | |
| | pad = Segment(" " * excess_space, style) if self.pad else None |
| | for line in lines: |
| | yield from line |
| | if pad: |
| | yield pad |
| | yield new_line |
| |
|
| | elif align == "center": |
| | |
| | left = excess_space // 2 |
| | pad = Segment(" " * left, style) |
| | pad_right = ( |
| | Segment(" " * (excess_space - left), style) if self.pad else None |
| | ) |
| | for line in lines: |
| | if left: |
| | yield pad |
| | yield from line |
| | if pad_right: |
| | yield pad_right |
| | yield new_line |
| |
|
| | elif align == "right": |
| | |
| | pad = Segment(" " * excess_space, style) |
| | for line in lines: |
| | yield pad |
| | yield from line |
| | yield new_line |
| |
|
| | blank_line = ( |
| | Segment(f"{' ' * (self.width or options.max_width)}\n", style) |
| | if self.pad |
| | else Segment("\n") |
| | ) |
| |
|
| | def blank_lines(count: int) -> Iterable[Segment]: |
| | if count > 0: |
| | for _ in range(count): |
| | yield blank_line |
| |
|
| | vertical_height = self.height or options.height |
| | iter_segments: Iterable[Segment] |
| | if self.vertical and vertical_height is not None: |
| | if self.vertical == "top": |
| | bottom_space = vertical_height - height |
| | iter_segments = chain(generate_segments(), blank_lines(bottom_space)) |
| | elif self.vertical == "middle": |
| | top_space = (vertical_height - height) // 2 |
| | bottom_space = vertical_height - top_space - height |
| | iter_segments = chain( |
| | blank_lines(top_space), |
| | generate_segments(), |
| | blank_lines(bottom_space), |
| | ) |
| | else: |
| | top_space = vertical_height - height |
| | iter_segments = chain(blank_lines(top_space), generate_segments()) |
| | else: |
| | iter_segments = generate_segments() |
| | if self.style: |
| | style = console.get_style(self.style) |
| | iter_segments = Segment.apply_style(iter_segments, style) |
| | yield from iter_segments |
| |
|
| | def __rich_measure__( |
| | self, console: "Console", options: "ConsoleOptions" |
| | ) -> Measurement: |
| | measurement = Measurement.get(console, options, self.renderable) |
| | return measurement |
| |
|
| |
|
| | class VerticalCenter(JupyterMixin): |
| | """Vertically aligns a renderable. |
| | |
| | Warn: |
| | This class is deprecated and may be removed in a future version. Use Align class with |
| | `vertical="middle"`. |
| | |
| | Args: |
| | renderable (RenderableType): A renderable object. |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | renderable: "RenderableType", |
| | style: Optional[StyleType] = None, |
| | ) -> None: |
| | self.renderable = renderable |
| | self.style = style |
| |
|
| | def __repr__(self) -> str: |
| | return f"VerticalCenter({self.renderable!r})" |
| |
|
| | def __rich_console__( |
| | self, console: "Console", options: "ConsoleOptions" |
| | ) -> "RenderResult": |
| | style = console.get_style(self.style) if self.style is not None else None |
| | lines = console.render_lines( |
| | self.renderable, options.update(height=None), pad=False |
| | ) |
| | width, _height = Segment.get_shape(lines) |
| | new_line = Segment.line() |
| | height = options.height or options.size.height |
| | top_space = (height - len(lines)) // 2 |
| | bottom_space = height - top_space - len(lines) |
| | blank_line = Segment(f"{' ' * width}", style) |
| |
|
| | def blank_lines(count: int) -> Iterable[Segment]: |
| | for _ in range(count): |
| | yield blank_line |
| | yield new_line |
| |
|
| | if top_space > 0: |
| | yield from blank_lines(top_space) |
| | for line in lines: |
| | yield from line |
| | yield new_line |
| | if bottom_space > 0: |
| | yield from blank_lines(bottom_space) |
| |
|
| | def __rich_measure__( |
| | self, console: "Console", options: "ConsoleOptions" |
| | ) -> Measurement: |
| | measurement = Measurement.get(console, options, self.renderable) |
| | return measurement |
| |
|
| |
|
| | if __name__ == "__main__": |
| | from pip._vendor.rich.console import Console, Group |
| | from pip._vendor.rich.highlighter import ReprHighlighter |
| | from pip._vendor.rich.panel import Panel |
| |
|
| | highlighter = ReprHighlighter() |
| | console = Console() |
| |
|
| | panel = Panel( |
| | Group( |
| | Align.left(highlighter("align='left'")), |
| | Align.center(highlighter("align='center'")), |
| | Align.right(highlighter("align='right'")), |
| | ), |
| | width=60, |
| | style="on dark_blue", |
| | title="Algin", |
| | ) |
| |
|
| | console.print( |
| | Align.center(panel, vertical="middle", style="on red", height=console.height) |
| | ) |
| |
|