containers.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. from itertools import zip_longest
  2. from typing import (
  3. Iterator,
  4. Iterable,
  5. List,
  6. Optional,
  7. Union,
  8. overload,
  9. TypeVar,
  10. TYPE_CHECKING,
  11. )
  12. if TYPE_CHECKING:
  13. from .console import (
  14. Console,
  15. ConsoleOptions,
  16. JustifyMethod,
  17. OverflowMethod,
  18. RenderResult,
  19. RenderableType,
  20. )
  21. from .text import Text
  22. from .cells import cell_len
  23. from .measure import Measurement
  24. T = TypeVar("T")
  25. class Renderables:
  26. """A list subclass which renders its contents to the console."""
  27. def __init__(
  28. self, renderables: Optional[Iterable["RenderableType"]] = None
  29. ) -> None:
  30. self._renderables: List["RenderableType"] = (
  31. list(renderables) if renderables is not None else []
  32. )
  33. def __rich_console__(
  34. self, console: "Console", options: "ConsoleOptions"
  35. ) -> "RenderResult":
  36. """Console render method to insert line-breaks."""
  37. yield from self._renderables
  38. def __rich_measure__(
  39. self, console: "Console", options: "ConsoleOptions"
  40. ) -> "Measurement":
  41. dimensions = [
  42. Measurement.get(console, options, renderable)
  43. for renderable in self._renderables
  44. ]
  45. if not dimensions:
  46. return Measurement(1, 1)
  47. _min = max(dimension.minimum for dimension in dimensions)
  48. _max = max(dimension.maximum for dimension in dimensions)
  49. return Measurement(_min, _max)
  50. def append(self, renderable: "RenderableType") -> None:
  51. self._renderables.append(renderable)
  52. def __iter__(self) -> Iterable["RenderableType"]:
  53. return iter(self._renderables)
  54. class Lines:
  55. """A list subclass which can render to the console."""
  56. def __init__(self, lines: Iterable["Text"] = ()) -> None:
  57. self._lines: List["Text"] = list(lines)
  58. def __repr__(self) -> str:
  59. return f"Lines({self._lines!r})"
  60. def __iter__(self) -> Iterator["Text"]:
  61. return iter(self._lines)
  62. @overload
  63. def __getitem__(self, index: int) -> "Text":
  64. ...
  65. @overload
  66. def __getitem__(self, index: slice) -> List["Text"]:
  67. ...
  68. def __getitem__(self, index: Union[slice, int]) -> Union["Text", List["Text"]]:
  69. return self._lines[index]
  70. def __setitem__(self, index: int, value: "Text") -> "Lines":
  71. self._lines[index] = value
  72. return self
  73. def __len__(self) -> int:
  74. return self._lines.__len__()
  75. def __rich_console__(
  76. self, console: "Console", options: "ConsoleOptions"
  77. ) -> "RenderResult":
  78. """Console render method to insert line-breaks."""
  79. yield from self._lines
  80. def append(self, line: "Text") -> None:
  81. self._lines.append(line)
  82. def extend(self, lines: Iterable["Text"]) -> None:
  83. self._lines.extend(lines)
  84. def pop(self, index: int = -1) -> "Text":
  85. return self._lines.pop(index)
  86. def justify(
  87. self,
  88. console: "Console",
  89. width: int,
  90. justify: "JustifyMethod" = "left",
  91. overflow: "OverflowMethod" = "fold",
  92. ) -> None:
  93. """Justify and overflow text to a given width.
  94. Args:
  95. console (Console): Console instance.
  96. width (int): Number of characters per line.
  97. justify (str, optional): Default justify method for text: "left", "center", "full" or "right". Defaults to "left".
  98. overflow (str, optional): Default overflow for text: "crop", "fold", or "ellipsis". Defaults to "fold".
  99. """
  100. from .text import Text
  101. if justify == "left":
  102. for line in self._lines:
  103. line.truncate(width, overflow=overflow, pad=True)
  104. elif justify == "center":
  105. for line in self._lines:
  106. line.rstrip()
  107. line.truncate(width, overflow=overflow)
  108. line.pad_left((width - cell_len(line.plain)) // 2)
  109. line.pad_right(width - cell_len(line.plain))
  110. elif justify == "right":
  111. for line in self._lines:
  112. line.rstrip()
  113. line.truncate(width, overflow=overflow)
  114. line.pad_left(width - cell_len(line.plain))
  115. elif justify == "full":
  116. for line_index, line in enumerate(self._lines):
  117. if line_index == len(self._lines) - 1:
  118. break
  119. words = line.split(" ")
  120. words_size = sum(cell_len(word.plain) for word in words)
  121. num_spaces = len(words) - 1
  122. spaces = [1 for _ in range(num_spaces)]
  123. index = 0
  124. if spaces:
  125. while words_size + num_spaces < width:
  126. spaces[len(spaces) - index - 1] += 1
  127. num_spaces += 1
  128. index = (index + 1) % len(spaces)
  129. tokens: List[Text] = []
  130. for index, (word, next_word) in enumerate(
  131. zip_longest(words, words[1:])
  132. ):
  133. tokens.append(word)
  134. if index < len(spaces):
  135. style = word.get_style_at_offset(console, -1)
  136. next_style = next_word.get_style_at_offset(console, 0)
  137. space_style = style if style == next_style else line.style
  138. tokens.append(Text(" " * spaces[index], style=space_style))
  139. self[line_index] = Text("").join(tokens)