mirror of https://github.com/Textualize/rich.git
commit
b49faf271a
|
@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Added ascii_only to ConsoleOptions
|
||||
- Addded box.SQUARE_DOUBLE_HEAD
|
||||
- Added highlighting of EUI-48 and EUI-64 (MAC addresses)
|
||||
- Added Console.pager
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -210,6 +210,25 @@ An alternative way of capturing output is to set the Console file to a :py:class
|
|||
console.print("[bold red]Hello[/] World")
|
||||
str_output = console.file.getvalue()
|
||||
|
||||
Paging
|
||||
------
|
||||
|
||||
If you have some long output to present to the user you can use a *pager* to display it. A pager is typically an application on by your operating system which will at least support pressing a key to scroll, but will often support scrolling up and down through the text and other features.
|
||||
|
||||
You can page output from a Console by calling :meth:`~rich.console.Console.pager` which returns a context manger. When the pager exits, anything that was printed will be sent to the pager. Here's an example::
|
||||
|
||||
from rich.__main__ import make_test_card
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
with console.pager():
|
||||
console.print(make_test_card())
|
||||
|
||||
Since the default pager on most platforms don't support color, Rich will strip color from the output. If you know that your pager supports color, you can set ``style=True`` when calling the :meth:`~rich.console.Console.pager` method.
|
||||
|
||||
.. note::
|
||||
Rich will use the ``PAGER`` environment variable to get the pager command. On Linux and macOS you can set this to ``less -r`` to enable paging with ANSI styles.
|
||||
|
||||
|
||||
Terminal detection
|
||||
------------------
|
||||
|
|
|
@ -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,40 @@ 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)
|
||||
self._console._exit_buffer()
|
||||
|
||||
|
||||
class RenderGroup:
|
||||
"""Takes a group of renderables and returns a renderable object that renders the group.
|
||||
|
||||
|
@ -523,7 +558,7 @@ class Console:
|
|||
Returns:
|
||||
str: Console output.
|
||||
"""
|
||||
render_result = self._render_buffer()
|
||||
render_result = self._render_buffer(self._buffer)
|
||||
del self._buffer[:]
|
||||
self._exit_buffer()
|
||||
return render_result
|
||||
|
@ -667,6 +702,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 +1212,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 +1228,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:
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
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(Pager):
|
||||
"""Uses the pager installed on the system."""
|
||||
|
||||
_pager = lambda self, content: pydoc.pager(content)
|
||||
|
||||
def show(self, content: str) -> None:
|
||||
"""Use the same pager used by pydoc."""
|
||||
self._pager(content)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from .__main__ import make_test_card
|
||||
from .console import Console
|
||||
|
||||
console = Console()
|
||||
with console.pager(styles=True):
|
||||
console.print(make_test_card())
|
|
@ -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.update_link(None) if style else 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")]]
|
||||
|
|
|
@ -522,6 +522,28 @@ class Style:
|
|||
style._null = False
|
||||
return style
|
||||
|
||||
def update_link(self, link: str = None) -> "Style":
|
||||
"""Get a copy with a different value for link.
|
||||
|
||||
Args:
|
||||
link (str, optional): New value for link. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Style: A new Style instance.
|
||||
"""
|
||||
style = self.__new__(Style)
|
||||
style._ansi = self._ansi
|
||||
style._style_definition = self._style_definition
|
||||
style._color = self._color
|
||||
style._bgcolor = self._bgcolor
|
||||
style._attributes = self._attributes
|
||||
style._set_attributes = self._set_attributes
|
||||
style._link = link
|
||||
style._link_id = f"{time()}-{randint(0, 999999)}" if link else ""
|
||||
style._hash = self._hash
|
||||
style._null = False
|
||||
return style
|
||||
|
||||
def render(
|
||||
self,
|
||||
text: str = "",
|
||||
|
|
|
@ -171,7 +171,7 @@ class Table(JupyterMixin):
|
|||
self.footer_style = footer_style
|
||||
self.border_style = border_style
|
||||
self.title_style = title_style
|
||||
self.caption_style = title_style
|
||||
self.caption_style = caption_style
|
||||
self.title_justify = title_justify
|
||||
self.caption_justify = caption_justify
|
||||
self._row_count = 0
|
||||
|
@ -241,12 +241,6 @@ class Table(JupyterMixin):
|
|||
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
|
||||
if self.width is not None:
|
||||
max_width = self.width
|
||||
|
||||
# if self.box:
|
||||
# max_width -= len(self.columns) - 1
|
||||
# if self.show_edge:
|
||||
# max_width -= 2
|
||||
|
||||
if max_width < 0:
|
||||
return Measurement(0, 0)
|
||||
|
||||
|
|
|
@ -2,12 +2,14 @@ import io
|
|||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from rich import errors
|
||||
from rich.color import ColorSystem
|
||||
from rich.console import CaptureError, Console, ConsoleOptions
|
||||
from rich import errors
|
||||
from rich.pager import SystemPager
|
||||
from rich.panel import Panel
|
||||
from rich.style import Style
|
||||
|
||||
|
@ -346,3 +348,25 @@ def test_bell() -> None:
|
|||
console.begin_capture()
|
||||
console.bell()
|
||||
assert console.end_capture() == "\x07"
|
||||
|
||||
|
||||
def test_pager() -> None:
|
||||
console = Console()
|
||||
|
||||
pager_content: Optional[str] = None
|
||||
|
||||
def mock_pager(content: str) -> None:
|
||||
nonlocal pager_content
|
||||
pager_content = content
|
||||
|
||||
pager = SystemPager()
|
||||
pager._pager = mock_pager
|
||||
|
||||
with console.pager(pager):
|
||||
console.print("[bold]Hello World")
|
||||
assert pager_content == "Hello World\n"
|
||||
|
||||
with console.pager(pager, styles=True, links=False):
|
||||
console.print("[bold link https:/example.org]Hello World")
|
||||
|
||||
assert pager_content == "Hello World\n"
|
||||
|
|
|
@ -86,3 +86,13 @@ def test_filter_control():
|
|||
assert list(Segment.filter_control(segments, is_control=True)) == [
|
||||
Segment("bar", is_control=True)
|
||||
]
|
||||
|
||||
|
||||
def test_strip_styles():
|
||||
segments = [Segment("foo", Style(bold=True))]
|
||||
assert list(Segment.strip_styles(segments)) == [Segment("foo", None)]
|
||||
|
||||
|
||||
def test_strip_links():
|
||||
segments = [Segment("foo", Style(bold=True, link="https://www.example.org"))]
|
||||
assert list(Segment.strip_links(segments)) == [Segment("foo", Style(bold=True))]
|
||||
|
|
Loading…
Reference in New Issue