pager functionality

This commit is contained in:
Will McGugan 2020-10-10 15:04:19 +01:00
parent af7fbf9a2e
commit dd5ff93fe9
4 changed files with 119 additions and 7 deletions

View File

@ -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:

31
rich/pager.py Normal file
View File

@ -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())

View File

@ -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")]]

View File

@ -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