123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- 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 # pragma: no cover
- 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"]
- AlignValues = AlignMethod # TODO: deprecate AlignValues
- 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:
- # Exact fit
- for line in lines:
- yield from line
- yield new_line
- elif align == "left":
- # Pad on the right
- 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":
- # Pad left and right
- 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":
- # Padding on left
- 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: # self.vertical == "bottom":
- 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__": # pragma: no cover
- 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)
- )
|