fancy.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. from typing import Any, Dict, List, Optional
  2. from rich._loop import loop_first_last
  3. from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
  4. from rich.segment import Segment
  5. from rich.style import Style
  6. from rich.text import Text
  7. from typing_extensions import Literal
  8. from rich_toolkit.container import Container
  9. from rich_toolkit.element import CursorOffset, Element
  10. from rich_toolkit.form import Form
  11. from rich_toolkit.progress import Progress
  12. from rich_toolkit.styles.base import BaseStyle
  13. class FancyPanel:
  14. def __init__(
  15. self,
  16. renderable: RenderableType,
  17. style: BaseStyle,
  18. title: Optional[str] = None,
  19. metadata: Optional[Dict[str, Any]] = None,
  20. is_animated: Optional[bool] = None,
  21. animation_counter: Optional[int] = None,
  22. done: bool = False,
  23. ) -> None:
  24. self.renderable = renderable
  25. self._title = title
  26. self.metadata = metadata or {}
  27. self.width = None
  28. self.expand = True
  29. self.is_animated = is_animated
  30. self.counter = animation_counter or 0
  31. self.style = style
  32. self.done = done
  33. def _get_decoration(self, suffix: str = "") -> Segment:
  34. char = "┌" if self.metadata.get("title") else "◆"
  35. animated = not self.done and self.is_animated
  36. animation_status: Literal["started", "stopped", "error"] = (
  37. "started" if animated else "stopped"
  38. )
  39. color = self.style._get_animation_colors(
  40. steps=14, breathe=True, animation_status=animation_status
  41. )[self.counter % 14]
  42. return Segment(char + suffix, style=Style.from_color(color))
  43. def _strip_trailing_newlines(
  44. self, lines: List[List[Segment]]
  45. ) -> List[List[Segment]]:
  46. # remove all empty lines from the end of the list
  47. while lines and all(segment.text.strip() == "" for segment in lines[-1]):
  48. lines.pop()
  49. return lines
  50. def __rich_console__(
  51. self, console: "Console", options: "ConsoleOptions"
  52. ) -> "RenderResult":
  53. renderable = self.renderable
  54. lines = console.render_lines(renderable)
  55. lines = self._strip_trailing_newlines(lines)
  56. line_start = self._get_decoration()
  57. new_line = Segment.line()
  58. if self._title is not None:
  59. yield line_start
  60. yield Segment(" ")
  61. yield self._title
  62. for first, last, line in loop_first_last(lines):
  63. if first and not self._title:
  64. decoration = (
  65. Segment("┌ ")
  66. if self.metadata.get("title", False)
  67. else self._get_decoration(suffix=" ")
  68. )
  69. elif last and self.metadata.get("started", True):
  70. decoration = Segment("└ ")
  71. else:
  72. decoration = Segment("│ ")
  73. yield decoration
  74. yield from line
  75. if not last:
  76. yield new_line
  77. class FancyStyle(BaseStyle):
  78. _should_show_progress_title = False
  79. def __init__(self, *args, **kwargs) -> None:
  80. super().__init__(*args, **kwargs)
  81. self.cursor_offset = 2
  82. self.decoration_size = 2
  83. def _should_decorate(self, element: Any, parent: Optional[Element] = None) -> bool:
  84. return not isinstance(parent, (Progress, Container))
  85. def render_element(
  86. self,
  87. element: Any,
  88. is_active: bool = False,
  89. done: bool = False,
  90. parent: Optional[Element] = None,
  91. **metadata: Any,
  92. ) -> RenderableType:
  93. title: Optional[str] = None
  94. is_animated = False
  95. if isinstance(element, Progress):
  96. title = element.title
  97. is_animated = True
  98. rendered = super().render_element(
  99. element=element, is_active=is_active, done=done, parent=parent, **metadata
  100. )
  101. if self._should_decorate(element, parent):
  102. rendered = FancyPanel(
  103. rendered,
  104. title=title,
  105. metadata=metadata,
  106. is_animated=is_animated,
  107. done=done,
  108. animation_counter=self.animation_counter,
  109. style=self,
  110. )
  111. return rendered
  112. def empty_line(self) -> Text:
  113. """Return an empty line with decoration.
  114. Returns:
  115. A text object representing an empty line
  116. """
  117. return Text("│", style="fancy.normal")
  118. def get_cursor_offset_for_element(
  119. self, element: Element, parent: Optional[Element] = None
  120. ) -> CursorOffset:
  121. """Get the cursor offset for an element.
  122. Args:
  123. element: The element to get the cursor offset for
  124. Returns:
  125. The cursor offset
  126. """
  127. if isinstance(element, Form):
  128. return element.cursor_offset
  129. else:
  130. return CursorOffset(
  131. top=element.cursor_offset.top,
  132. left=self.decoration_size + element.cursor_offset.left,
  133. )