diff --git a/rich/console.py b/rich/console.py index f9f7ba80..375a6b8d 100644 --- a/rich/console.py +++ b/rich/console.py @@ -35,6 +35,7 @@ from .control import Control from .highlighter import NullHighlighter, ReprHighlighter from .markup import render as render_markup from .measure import Measurement, measure_renderables +from .pager import Pager, SystemPager from .pretty import Pretty from .scope import render_scope from .segment import Segment @@ -205,6 +206,39 @@ class ThemeContext: self.console.pop_theme() +class PagerContext: + """A context manager that 'pages' content. See :meth:`~rich.console.Console.pager` for usage.""" + + def __init__( + self, + console: "Console", + pager: Pager = None, + styles: bool = False, + links: bool = False, + ) -> None: + self._console = console + self.pager = SystemPager() if pager is None else pager + self.styles = styles + self.links = links + + def __enter__(self) -> "PagerContext": + self._console._enter_buffer() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + if exc_type is None: + with self._console._lock: + buffer: List[Segment] = self._console._buffer[:] + del self._console._buffer[:] + segments: Iterable[Segment] = buffer + if not self.styles: + segments = Segment.strip_styles(segments) + elif not self.links: + segments = Segment.strip_links(segments) + content = self._console._render_buffer(segments) + self.pager.show(content) + + class RenderGroup: """Takes a group of renderables and returns a renderable object that renders the group. @@ -667,6 +701,29 @@ class Console: capture = Capture(self) return capture + def pager( + self, pager: Pager = None, styles: bool = False, links: bool = False + ) -> PagerContext: + """A context manager to display anything printed within a "pager". The pager used + is defined by the system and will typically support at less pressing a key to scroll. + + Args: + pager (Pager, optional): A pager object, or None to use :class:~rich.pager.SystemPager`. Defaults to None. + styles (bool, optional): Show styles in pager. Defaults to False. + links (bool, optional): Show links in pager. Defaults to False. + + Example: + >>> from rich.console import Console + >>> from rich.__main__ import make_test_card + >>> console = Console() + >>> with console.pager(): + console.print(make_test_card()) + + Returns: + PagerContext: A context manager. + """ + return PagerContext(self, pager=pager, styles=styles, links=links) + def line(self, count: int = 1) -> None: """Write new line(s). @@ -1154,7 +1211,8 @@ class Console: display(self._buffer) del self._buffer[:] else: - text = self._render_buffer() + text = self._render_buffer(self._buffer[:]) + del self._buffer[:] if text: try: if WINDOWS: # pragma: no cover @@ -1169,17 +1227,15 @@ class Console: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise - def _render_buffer(self) -> str: + def _render_buffer(self, buffer: Iterable[Segment]) -> str: """Render buffered output, and clear buffer.""" output: List[str] = [] append = output.append color_system = self._color_system legacy_windows = self.legacy_windows - buffer = self._buffer[:] if self.record: with self._record_buffer_lock: self._record_buffer.extend(buffer) - del self._buffer[:] not_terminal = not self.is_terminal for text, style, is_control in buffer: if style and not is_control: diff --git a/rich/pager.py b/rich/pager.py new file mode 100644 index 00000000..c1aa5b23 --- /dev/null +++ b/rich/pager.py @@ -0,0 +1,31 @@ +from abc import ABC, abstractmethod +import pydoc + + +class Pager(ABC): + """Base class for a pager.""" + + @abstractmethod + def show(self, content: str) -> None: + """Show content in pager. + + Args: + content (str): Content to be displayed. + """ + + +class SystemPager: + """Uses the pager installed on the system.""" + + def show(self, content: str) -> None: + """Use the same pager used by pydoc.""" + pydoc.pager(content) + + +if __name__ == "__main__": # pragma: no cover + from .__main__ import make_test_card + from .console import Console + + console = Console() + with console.pager(): + console.print(make_test_card()) diff --git a/rich/segment.py b/rich/segment.py index 21c91321..a63e2534 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -300,6 +300,30 @@ class Segment(NamedTuple): last_segment = segment yield last_segment + @classmethod + def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: + """Remove all links from an iterable of styles. + + Yields: + Segment: Segments with link removed. + """ + for segment in segments: + if segment.is_control or segment.style is None: + yield segment + else: + text, style, _is_control = segment + yield cls(text, style.copy(link=None)) + + @classmethod + def strip_styles(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: + """Remove all styles from an iterable of segments. + + Yields: + Segment: Segments with styles replace with None + """ + for text, _style, is_control in segments: + yield cls(text, None, is_control) + if __name__ == "__main__": # pragma: no cover lines = [[Segment("Hello")]] diff --git a/rich/style.py b/rich/style.py index a67e2c67..707324a9 100644 --- a/rich/style.py +++ b/rich/style.py @@ -501,7 +501,7 @@ class Style: iter_styles = iter(styles) return sum(iter_styles, next(iter_styles)) - def copy(self) -> "Style": + def copy(self, link: str = None) -> "Style": """Get a copy of this style. Returns: @@ -516,8 +516,9 @@ class Style: style._bgcolor = self._bgcolor style._attributes = self._attributes style._set_attributes = self._set_attributes - style._link = self._link - style._link_id = f"{time()}-{randint(0, 999999)}" if self._link else "" + _link = self._link if link is None else link + style._link = _link + style._link_id = f"{time()}-{randint(0, 999999)}" if _link else "" style._hash = self._hash style._null = False return style