|
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) |
|
) |
|
|