align.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import sys
  2. from itertools import chain
  3. from typing import TYPE_CHECKING, Iterable, Optional
  4. if sys.version_info >= (3, 8):
  5. from typing import Literal
  6. else:
  7. from pip._vendor.typing_extensions import Literal # pragma: no cover
  8. from .constrain import Constrain
  9. from .jupyter import JupyterMixin
  10. from .measure import Measurement
  11. from .segment import Segment
  12. from .style import StyleType
  13. if TYPE_CHECKING:
  14. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  15. AlignMethod = Literal["left", "center", "right"]
  16. VerticalAlignMethod = Literal["top", "middle", "bottom"]
  17. class Align(JupyterMixin):
  18. """Align a renderable by adding spaces if necessary.
  19. Args:
  20. renderable (RenderableType): A console renderable.
  21. align (AlignMethod): One of "left", "center", or "right""
  22. style (StyleType, optional): An optional style to apply to the background.
  23. vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
  24. pad (bool, optional): Pad the right with spaces. Defaults to True.
  25. width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
  26. height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
  27. Raises:
  28. ValueError: if ``align`` is not one of the expected values.
  29. """
  30. def __init__(
  31. self,
  32. renderable: "RenderableType",
  33. align: AlignMethod = "left",
  34. style: Optional[StyleType] = None,
  35. *,
  36. vertical: Optional[VerticalAlignMethod] = None,
  37. pad: bool = True,
  38. width: Optional[int] = None,
  39. height: Optional[int] = None,
  40. ) -> None:
  41. if align not in ("left", "center", "right"):
  42. raise ValueError(
  43. f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
  44. )
  45. if vertical is not None and vertical not in ("top", "middle", "bottom"):
  46. raise ValueError(
  47. f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
  48. )
  49. self.renderable = renderable
  50. self.align = align
  51. self.style = style
  52. self.vertical = vertical
  53. self.pad = pad
  54. self.width = width
  55. self.height = height
  56. def __repr__(self) -> str:
  57. return f"Align({self.renderable!r}, {self.align!r})"
  58. @classmethod
  59. def left(
  60. cls,
  61. renderable: "RenderableType",
  62. style: Optional[StyleType] = None,
  63. *,
  64. vertical: Optional[VerticalAlignMethod] = None,
  65. pad: bool = True,
  66. width: Optional[int] = None,
  67. height: Optional[int] = None,
  68. ) -> "Align":
  69. """Align a renderable to the left."""
  70. return cls(
  71. renderable,
  72. "left",
  73. style=style,
  74. vertical=vertical,
  75. pad=pad,
  76. width=width,
  77. height=height,
  78. )
  79. @classmethod
  80. def center(
  81. cls,
  82. renderable: "RenderableType",
  83. style: Optional[StyleType] = None,
  84. *,
  85. vertical: Optional[VerticalAlignMethod] = None,
  86. pad: bool = True,
  87. width: Optional[int] = None,
  88. height: Optional[int] = None,
  89. ) -> "Align":
  90. """Align a renderable to the center."""
  91. return cls(
  92. renderable,
  93. "center",
  94. style=style,
  95. vertical=vertical,
  96. pad=pad,
  97. width=width,
  98. height=height,
  99. )
  100. @classmethod
  101. def right(
  102. cls,
  103. renderable: "RenderableType",
  104. style: Optional[StyleType] = None,
  105. *,
  106. vertical: Optional[VerticalAlignMethod] = None,
  107. pad: bool = True,
  108. width: Optional[int] = None,
  109. height: Optional[int] = None,
  110. ) -> "Align":
  111. """Align a renderable to the right."""
  112. return cls(
  113. renderable,
  114. "right",
  115. style=style,
  116. vertical=vertical,
  117. pad=pad,
  118. width=width,
  119. height=height,
  120. )
  121. def __rich_console__(
  122. self, console: "Console", options: "ConsoleOptions"
  123. ) -> "RenderResult":
  124. align = self.align
  125. width = console.measure(self.renderable, options=options).maximum
  126. rendered = console.render(
  127. Constrain(
  128. self.renderable, width if self.width is None else min(width, self.width)
  129. ),
  130. options.update(height=None),
  131. )
  132. lines = list(Segment.split_lines(rendered))
  133. width, height = Segment.get_shape(lines)
  134. lines = Segment.set_shape(lines, width, height)
  135. new_line = Segment.line()
  136. excess_space = options.max_width - width
  137. style = console.get_style(self.style) if self.style is not None else None
  138. def generate_segments() -> Iterable[Segment]:
  139. if excess_space <= 0:
  140. # Exact fit
  141. for line in lines:
  142. yield from line
  143. yield new_line
  144. elif align == "left":
  145. # Pad on the right
  146. pad = Segment(" " * excess_space, style) if self.pad else None
  147. for line in lines:
  148. yield from line
  149. if pad:
  150. yield pad
  151. yield new_line
  152. elif align == "center":
  153. # Pad left and right
  154. left = excess_space // 2
  155. pad = Segment(" " * left, style)
  156. pad_right = (
  157. Segment(" " * (excess_space - left), style) if self.pad else None
  158. )
  159. for line in lines:
  160. if left:
  161. yield pad
  162. yield from line
  163. if pad_right:
  164. yield pad_right
  165. yield new_line
  166. elif align == "right":
  167. # Padding on left
  168. pad = Segment(" " * excess_space, style)
  169. for line in lines:
  170. yield pad
  171. yield from line
  172. yield new_line
  173. blank_line = (
  174. Segment(f"{' ' * (self.width or options.max_width)}\n", style)
  175. if self.pad
  176. else Segment("\n")
  177. )
  178. def blank_lines(count: int) -> Iterable[Segment]:
  179. if count > 0:
  180. for _ in range(count):
  181. yield blank_line
  182. vertical_height = self.height or options.height
  183. iter_segments: Iterable[Segment]
  184. if self.vertical and vertical_height is not None:
  185. if self.vertical == "top":
  186. bottom_space = vertical_height - height
  187. iter_segments = chain(generate_segments(), blank_lines(bottom_space))
  188. elif self.vertical == "middle":
  189. top_space = (vertical_height - height) // 2
  190. bottom_space = vertical_height - top_space - height
  191. iter_segments = chain(
  192. blank_lines(top_space),
  193. generate_segments(),
  194. blank_lines(bottom_space),
  195. )
  196. else: # self.vertical == "bottom":
  197. top_space = vertical_height - height
  198. iter_segments = chain(blank_lines(top_space), generate_segments())
  199. else:
  200. iter_segments = generate_segments()
  201. if self.style:
  202. style = console.get_style(self.style)
  203. iter_segments = Segment.apply_style(iter_segments, style)
  204. yield from iter_segments
  205. def __rich_measure__(
  206. self, console: "Console", options: "ConsoleOptions"
  207. ) -> Measurement:
  208. measurement = Measurement.get(console, options, self.renderable)
  209. return measurement
  210. class VerticalCenter(JupyterMixin):
  211. """Vertically aligns a renderable.
  212. Warn:
  213. This class is deprecated and may be removed in a future version. Use Align class with
  214. `vertical="middle"`.
  215. Args:
  216. renderable (RenderableType): A renderable object.
  217. """
  218. def __init__(
  219. self,
  220. renderable: "RenderableType",
  221. style: Optional[StyleType] = None,
  222. ) -> None:
  223. self.renderable = renderable
  224. self.style = style
  225. def __repr__(self) -> str:
  226. return f"VerticalCenter({self.renderable!r})"
  227. def __rich_console__(
  228. self, console: "Console", options: "ConsoleOptions"
  229. ) -> "RenderResult":
  230. style = console.get_style(self.style) if self.style is not None else None
  231. lines = console.render_lines(
  232. self.renderable, options.update(height=None), pad=False
  233. )
  234. width, _height = Segment.get_shape(lines)
  235. new_line = Segment.line()
  236. height = options.height or options.size.height
  237. top_space = (height - len(lines)) // 2
  238. bottom_space = height - top_space - len(lines)
  239. blank_line = Segment(f"{' ' * width}", style)
  240. def blank_lines(count: int) -> Iterable[Segment]:
  241. for _ in range(count):
  242. yield blank_line
  243. yield new_line
  244. if top_space > 0:
  245. yield from blank_lines(top_space)
  246. for line in lines:
  247. yield from line
  248. yield new_line
  249. if bottom_space > 0:
  250. yield from blank_lines(bottom_space)
  251. def __rich_measure__(
  252. self, console: "Console", options: "ConsoleOptions"
  253. ) -> Measurement:
  254. measurement = Measurement.get(console, options, self.renderable)
  255. return measurement
  256. if __name__ == "__main__": # pragma: no cover
  257. from pip._vendor.rich.console import Console, Group
  258. from pip._vendor.rich.highlighter import ReprHighlighter
  259. from pip._vendor.rich.panel import Panel
  260. highlighter = ReprHighlighter()
  261. console = Console()
  262. panel = Panel(
  263. Group(
  264. Align.left(highlighter("align='left'")),
  265. Align.center(highlighter("align='center'")),
  266. Align.right(highlighter("align='right'")),
  267. ),
  268. width=60,
  269. style="on dark_blue",
  270. title="Align",
  271. )
  272. console.print(
  273. Align.center(panel, vertical="middle", style="on red", height=console.height)
  274. )