live_render.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. from typing import Literal, Optional, Tuple
  2. from ._loop import loop_last
  3. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  4. from .control import Control
  5. from .segment import ControlType, Segment
  6. from .style import StyleType
  7. from .text import Text
  8. VerticalOverflowMethod = Literal["crop", "ellipsis", "visible"]
  9. class LiveRender:
  10. """Creates a renderable that may be updated.
  11. Args:
  12. renderable (RenderableType): Any renderable object.
  13. style (StyleType, optional): An optional style to apply to the renderable. Defaults to "".
  14. """
  15. def __init__(
  16. self,
  17. renderable: RenderableType,
  18. style: StyleType = "",
  19. vertical_overflow: VerticalOverflowMethod = "ellipsis",
  20. ) -> None:
  21. self.renderable = renderable
  22. self.style = style
  23. self.vertical_overflow = vertical_overflow
  24. self._shape: Optional[Tuple[int, int]] = None
  25. @property
  26. def last_render_height(self) -> int:
  27. """The number of lines in the last render (may be 0 if nothing was rendered).
  28. Returns:
  29. Height in lines
  30. """
  31. if self._shape is None:
  32. return 0
  33. return self._shape[1]
  34. def set_renderable(self, renderable: RenderableType) -> None:
  35. """Set a new renderable.
  36. Args:
  37. renderable (RenderableType): Any renderable object, including str.
  38. """
  39. self.renderable = renderable
  40. def position_cursor(self) -> Control:
  41. """Get control codes to move cursor to beginning of live render.
  42. Returns:
  43. Control: A control instance that may be printed.
  44. """
  45. if self._shape is not None:
  46. _, height = self._shape
  47. return Control(
  48. ControlType.CARRIAGE_RETURN,
  49. (ControlType.ERASE_IN_LINE, 2),
  50. *(
  51. (
  52. (ControlType.CURSOR_UP, 1),
  53. (ControlType.ERASE_IN_LINE, 2),
  54. )
  55. * (height - 1)
  56. )
  57. )
  58. return Control()
  59. def restore_cursor(self) -> Control:
  60. """Get control codes to clear the render and restore the cursor to its previous position.
  61. Returns:
  62. Control: A Control instance that may be printed.
  63. """
  64. if self._shape is not None:
  65. _, height = self._shape
  66. return Control(
  67. ControlType.CARRIAGE_RETURN,
  68. *((ControlType.CURSOR_UP, 1), (ControlType.ERASE_IN_LINE, 2)) * height
  69. )
  70. return Control()
  71. def __rich_console__(
  72. self, console: Console, options: ConsoleOptions
  73. ) -> RenderResult:
  74. renderable = self.renderable
  75. style = console.get_style(self.style)
  76. lines = console.render_lines(renderable, options, style=style, pad=False)
  77. shape = Segment.get_shape(lines)
  78. _, height = shape
  79. if height > options.size.height:
  80. if self.vertical_overflow == "crop":
  81. lines = lines[: options.size.height]
  82. shape = Segment.get_shape(lines)
  83. elif self.vertical_overflow == "ellipsis":
  84. lines = lines[: (options.size.height - 1)]
  85. overflow_text = Text(
  86. "...",
  87. overflow="crop",
  88. justify="center",
  89. end="",
  90. style="live.ellipsis",
  91. )
  92. lines.append(list(console.render(overflow_text)))
  93. shape = Segment.get_shape(lines)
  94. self._shape = shape
  95. new_line = Segment.line()
  96. for last, line in loop_last(lines):
  97. yield from line
  98. if not last:
  99. yield new_line