codekingpro/portable-devtools
115k
1from itertools import chain2from typing import TYPE_CHECKING, Iterable, Optional, Literal3 4from .constrain import Constrain5from .jupyter import JupyterMixin6from .measure import Measurement7from .segment import Segment8from .style import StyleType9 10if TYPE_CHECKING:11 from .console import Console, ConsoleOptions, RenderableType, RenderResult12 13AlignMethod = Literal["left", "center", "right"]14VerticalAlignMethod = Literal["top", "middle", "bottom"]15 16 17class Align(JupyterMixin):18 """Align a renderable by adding spaces if necessary.19 20 Args:21 renderable (RenderableType): A console renderable.22 align (AlignMethod): One of "left", "center", or "right""23 style (StyleType, optional): An optional style to apply to the background.24 vertical (Optional[VerticalAlignMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.25 pad (bool, optional): Pad the right with spaces. Defaults to True.26 width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.27 height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.28 29 Raises:30 ValueError: if ``align`` is not one of the expected values.31 32 Example:33 .. code-block:: python34 35 from rich.console import Console36 from rich.align import Align37 from rich.panel import Panel38 39 console = Console()40 # Create a panel 20 characters wide41 p = Panel("Hello, [b]World[/b]!", style="on green", width=20)42 43 # Renders the panel centered in the terminal44 console.print(Align(p, align="center"))45 """46 47 def __init__(48 self,49 renderable: "RenderableType",50 align: AlignMethod = "left",51 style: Optional[StyleType] = None,52 *,53 vertical: Optional[VerticalAlignMethod] = None,54 pad: bool = True,55 width: Optional[int] = None,56 height: Optional[int] = None,57 ) -> None:58 if align not in ("left", "center", "right"):59 raise ValueError(60 f'invalid value for align, expected "left", "center", or "right" (not {align!r})'61 )62 if vertical is not None and vertical not in ("top", "middle", "bottom"):63 raise ValueError(64 f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'65 )66 self.renderable = renderable67 self.align = align68 self.style = style69 self.vertical = vertical70 self.pad = pad71 self.width = width72 self.height = height73 74 def __repr__(self) -> str:75 return f"Align({self.renderable!r}, {self.align!r})"76 77 @classmethod78 def left(79 cls,80 renderable: "RenderableType",81 style: Optional[StyleType] = None,82 *,83 vertical: Optional[VerticalAlignMethod] = None,84 pad: bool = True,85 width: Optional[int] = None,86 height: Optional[int] = None,87 ) -> "Align":88 """Align a renderable to the left."""89 return cls(90 renderable,91 "left",92 style=style,93 vertical=vertical,94 pad=pad,95 width=width,96 height=height,97 )98 99 @classmethod100 def center(101 cls,102 renderable: "RenderableType",103 style: Optional[StyleType] = None,104 *,105 vertical: Optional[VerticalAlignMethod] = None,106 pad: bool = True,107 width: Optional[int] = None,108 height: Optional[int] = None,109 ) -> "Align":110 """Align a renderable to the center."""111 return cls(112 renderable,113 "center",114 style=style,115 vertical=vertical,116 pad=pad,117 width=width,118 height=height,119 )120 121 @classmethod122 def right(123 cls,124 renderable: "RenderableType",125 style: Optional[StyleType] = None,126 *,127 vertical: Optional[VerticalAlignMethod] = None,128 pad: bool = True,129 width: Optional[int] = None,130 height: Optional[int] = None,131 ) -> "Align":132 """Align a renderable to the right."""133 return cls(134 renderable,135 "right",136 style=style,137 vertical=vertical,138 pad=pad,139 width=width,140 height=height,141 )142 143 def __rich_console__(144 self, console: "Console", options: "ConsoleOptions"145 ) -> "RenderResult":146 align = self.align147 width = console.measure(self.renderable, options=options).maximum148 rendered = console.render(149 Constrain(150 self.renderable, width if self.width is None else min(width, self.width)151 ),152 options.update(height=None),153 )154 lines = list(Segment.split_lines(rendered))155 width, height = Segment.get_shape(lines)156 lines = Segment.set_shape(lines, width, height)157 new_line = Segment.line()158 excess_space = options.max_width - width159 style = console.get_style(self.style) if self.style is not None else None160 161 def generate_segments() -> Iterable[Segment]:162 if excess_space <= 0:163 # Exact fit164 for line in lines:165 yield from line166 yield new_line167 168 elif align == "left":169 # Pad on the right170 pad = Segment(" " * excess_space, style) if self.pad else None171 for line in lines:172 yield from line173 if pad:174 yield pad175 yield new_line176 177 elif align == "center":178 # Pad left and right179 left = excess_space // 2180 pad = Segment(" " * left, style)181 pad_right = (182 Segment(" " * (excess_space - left), style) if self.pad else None183 )184 for line in lines:185 if left:186 yield pad187 yield from line188 if pad_right:189 yield pad_right190 yield new_line191 192 elif align == "right":193 # Padding on left194 pad = Segment(" " * excess_space, style)195 for line in lines:196 yield pad197 yield from line198 yield new_line199 200 blank_line = (201 Segment(f"{' ' * (self.width or options.max_width)}\n", style)202 if self.pad203 else Segment("\n")204 )205 206 def blank_lines(count: int) -> Iterable[Segment]:207 if count > 0:208 for _ in range(count):209 yield blank_line210 211 vertical_height = self.height or options.height212 iter_segments: Iterable[Segment]213 if self.vertical and vertical_height is not None:214 if self.vertical == "top":215 bottom_space = vertical_height - height216 iter_segments = chain(generate_segments(), blank_lines(bottom_space))217 elif self.vertical == "middle":218 top_space = (vertical_height - height) // 2219 bottom_space = vertical_height - top_space - height220 iter_segments = chain(221 blank_lines(top_space),222 generate_segments(),223 blank_lines(bottom_space),224 )225 else: # self.vertical == "bottom":226 top_space = vertical_height - height227 iter_segments = chain(blank_lines(top_space), generate_segments())228 else:229 iter_segments = generate_segments()230 if self.style:231 style = console.get_style(self.style)232 iter_segments = Segment.apply_style(iter_segments, style)233 yield from iter_segments234 235 def __rich_measure__(236 self, console: "Console", options: "ConsoleOptions"237 ) -> Measurement:238 measurement = Measurement.get(console, options, self.renderable)239 return measurement240 241 242class VerticalCenter(JupyterMixin):243 """Vertically aligns a renderable.244 245 Warn:246 This class is deprecated and may be removed in a future version. Use Align class with247 `vertical="middle"`.248 249 Args:250 renderable (RenderableType): A renderable object.251 style (StyleType, optional): An optional style to apply to the background. Defaults to None.252 """253 254 def __init__(255 self,256 renderable: "RenderableType",257 style: Optional[StyleType] = None,258 ) -> None:259 self.renderable = renderable260 self.style = style261 262 def __repr__(self) -> str:263 return f"VerticalCenter({self.renderable!r})"264 265 def __rich_console__(266 self, console: "Console", options: "ConsoleOptions"267 ) -> "RenderResult":268 style = console.get_style(self.style) if self.style is not None else None269 lines = console.render_lines(270 self.renderable, options.update(height=None), pad=False271 )272 width, _height = Segment.get_shape(lines)273 new_line = Segment.line()274 height = options.height or options.size.height275 top_space = (height - len(lines)) // 2276 bottom_space = height - top_space - len(lines)277 blank_line = Segment(f"{' ' * width}", style)278 279 def blank_lines(count: int) -> Iterable[Segment]:280 for _ in range(count):281 yield blank_line282 yield new_line283 284 if top_space > 0:285 yield from blank_lines(top_space)286 for line in lines:287 yield from line288 yield new_line289 if bottom_space > 0:290 yield from blank_lines(bottom_space)291 292 def __rich_measure__(293 self, console: "Console", options: "ConsoleOptions"294 ) -> Measurement:295 measurement = Measurement.get(console, options, self.renderable)296 return measurement297 298 299if __name__ == "__main__": # pragma: no cover300 from rich.console import Console, Group301 from rich.highlighter import ReprHighlighter302 from rich.panel import Panel303 304 highlighter = ReprHighlighter()305 console = Console()306 307 panel = Panel(308 Group(309 Align.left(highlighter("align='left'")),310 Align.center(highlighter("align='center'")),311 Align.right(highlighter("align='right'")),312 ),313 width=60,314 style="on dark_blue",315 title="Align",316 )317 318 console.print(319 Align.center(panel, vertical="middle", style="on red", height=console.height)320 )321 