| from enum import IntEnum |
| from functools import lru_cache |
| from itertools import filterfalse |
| from logging import getLogger |
| from operator import attrgetter |
| from typing import ( |
| TYPE_CHECKING, |
| Dict, |
| Iterable, |
| List, |
| NamedTuple, |
| Optional, |
| Sequence, |
| Tuple, |
| Type, |
| Union, |
| ) |
| |
| from .cells import ( |
| _is_single_cell_widths, |
| cell_len, |
| get_character_cell_size, |
| set_cell_size, |
| ) |
| from .repr import Result, rich_repr |
| from .style import Style |
| |
| if TYPE_CHECKING: |
| from .console import Console, ConsoleOptions, RenderResult |
| |
| log = getLogger("rich") |
| |
| |
| class ControlType(IntEnum): |
| """Non-printable control codes which typically translate to ANSI codes.""" |
| |
| BELL = 1 |
| CARRIAGE_RETURN = 2 |
| HOME = 3 |
| CLEAR = 4 |
| SHOW_CURSOR = 5 |
| HIDE_CURSOR = 6 |
| ENABLE_ALT_SCREEN = 7 |
| DISABLE_ALT_SCREEN = 8 |
| CURSOR_UP = 9 |
| CURSOR_DOWN = 10 |
| CURSOR_FORWARD = 11 |
| CURSOR_BACKWARD = 12 |
| CURSOR_MOVE_TO_COLUMN = 13 |
| CURSOR_MOVE_TO = 14 |
| ERASE_IN_LINE = 15 |
| |
| |
| ControlCode = Union[ |
| Tuple[ControlType], Tuple[ControlType, int], Tuple[ControlType, int, int] |
| ] |
| |
| |
| @rich_repr() |
| class Segment(NamedTuple): |
| """A piece of text with associated style. Segments are produced by the Console render process and |
| are ultimately converted in to strings to be written to the terminal. |
| |
| Args: |
| text (str): A piece of text. |
| style (:class:`~rich.style.Style`, optional): An optional style to apply to the text. |
| control (Tuple[ControlCode..], optional): Optional sequence of control codes. |
| """ |
| |
| text: str = "" |
| """Raw text.""" |
| style: Optional[Style] = None |
| """An optional style.""" |
| control: Optional[Sequence[ControlCode]] = None |
| """Optional sequence of control codes.""" |
| |
| def __rich_repr__(self) -> Result: |
| yield self.text |
| if self.control is None: |
| if self.style is not None: |
| yield self.style |
| else: |
| yield self.style |
| yield self.control |
| |
| def __bool__(self) -> bool: |
| """Check if the segment contains text.""" |
| return bool(self.text) |
| |
| @property |
| def cell_length(self) -> int: |
| """Get cell length of segment.""" |
| return 0 if self.control else cell_len(self.text) |
| |
| @property |
| def is_control(self) -> bool: |
| """Check if the segment contains control codes.""" |
| return self.control is not None |
| |
| @classmethod |
| @lru_cache(1024 * 16) |
| def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]: # type: ignore |
| |
| text, style, control = segment |
| _Segment = Segment |
| |
| cell_length = segment.cell_length |
| if cut >= cell_length: |
| return segment, _Segment("", style, control) |
| |
| cell_size = get_character_cell_size |
| |
| pos = int((cut / cell_length) * len(text)) |
| |
| before = text[:pos] |
| cell_pos = cell_len(before) |
| if cell_pos == cut: |
| return ( |
| _Segment(before, style, control), |
| _Segment(text[pos:], style, control), |
| ) |
| while pos < len(text): |
| char = text[pos] |
| pos += 1 |
| cell_pos += cell_size(char) |
| before = text[:pos] |
| if cell_pos == cut: |
| return ( |
| _Segment(before, style, control), |
| _Segment(text[pos:], style, control), |
| ) |
| if cell_pos > cut: |
| return ( |
| _Segment(before[: pos - 1] + " ", style, control), |
| _Segment(" " + text[pos:], style, control), |
| ) |
| |
| def split_cells(self, cut: int) -> Tuple["Segment", "Segment"]: |
| """Split segment in to two segments at the specified column. |
| |
| If the cut point falls in the middle of a 2-cell wide character then it is replaced |
| by two spaces, to preserve the display width of the parent segment. |
| |
| Returns: |
| Tuple[Segment, Segment]: Two segments. |
| """ |
| text, style, control = self |
| |
| if _is_single_cell_widths(text): |
| # Fast path with all 1 cell characters |
| if cut >= len(text): |
| return self, Segment("", style, control) |
| return ( |
| Segment(text[:cut], style, control), |
| Segment(text[cut:], style, control), |
| ) |
| |
| return self._split_cells(self, cut) |
| |
| @classmethod |
| def line(cls) -> "Segment": |
| """Make a new line segment.""" |
| return cls("\n") |
| |
| @classmethod |
| def apply_style( |
| cls, |
| segments: Iterable["Segment"], |
| style: Optional[Style] = None, |
| post_style: Optional[Style] = None, |
| ) -> Iterable["Segment"]: |
| """Apply style(s) to an iterable of segments. |
| |
| Returns an iterable of segments where the style is replaced by ``style + segment.style + post_style``. |
| |
| Args: |
| segments (Iterable[Segment]): Segments to process. |
| style (Style, optional): Base style. Defaults to None. |
| post_style (Style, optional): Style to apply on top of segment style. Defaults to None. |
| |
| Returns: |
| Iterable[Segments]: A new iterable of segments (possibly the same iterable). |
| """ |
| result_segments = segments |
| if style: |
| apply = style.__add__ |
| result_segments = ( |
| cls(text, None if control else apply(_style), control) |
| for text, _style, control in result_segments |
| ) |
| if post_style: |
| result_segments = ( |
| cls( |
| text, |
| ( |
| None |
| if control |
| else (_style + post_style if _style else post_style) |
| ), |
| control, |
| ) |
| for text, _style, control in result_segments |
| ) |
| return result_segments |
| |
| @classmethod |
| def filter_control( |
| cls, segments: Iterable["Segment"], is_control: bool = False |
| ) -> Iterable["Segment"]: |
| """Filter segments by ``is_control`` attribute. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable of Segment instances. |
| is_control (bool, optional): is_control flag to match in search. |
| |
| Returns: |
| Iterable[Segment]: And iterable of Segment instances. |
| |
| """ |
| if is_control: |
| return filter(attrgetter("control"), segments) |
| else: |
| return filterfalse(attrgetter("control"), segments) |
| |
| @classmethod |
| def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]: |
| """Split a sequence of segments in to a list of lines. |
| |
| Args: |
| segments (Iterable[Segment]): Segments potentially containing line feeds. |
| |
| Yields: |
| Iterable[List[Segment]]: Iterable of segment lists, one per line. |
| """ |
| line: List[Segment] = [] |
| append = line.append |
| |
| for segment in segments: |
| if "\n" in segment.text and not segment.control: |
| text, style, _ = segment |
| while text: |
| _text, new_line, text = text.partition("\n") |
| if _text: |
| append(cls(_text, style)) |
| if new_line: |
| yield line |
| line = [] |
| append = line.append |
| else: |
| append(segment) |
| if line: |
| yield line |
| |
| @classmethod |
| def split_and_crop_lines( |
| cls, |
| segments: Iterable["Segment"], |
| length: int, |
| style: Optional[Style] = None, |
| pad: bool = True, |
| include_new_lines: bool = True, |
| ) -> Iterable[List["Segment"]]: |
| """Split segments in to lines, and crop lines greater than a given length. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable of segments, probably |
| generated from console.render. |
| length (int): Desired line length. |
| style (Style, optional): Style to use for any padding. |
| pad (bool): Enable padding of lines that are less than `length`. |
| |
| Returns: |
| Iterable[List[Segment]]: An iterable of lines of segments. |
| """ |
| line: List[Segment] = [] |
| append = line.append |
| |
| adjust_line_length = cls.adjust_line_length |
| new_line_segment = cls("\n") |
| |
| for segment in segments: |
| if "\n" in segment.text and not segment.control: |
| text, style, _ = segment |
| while text: |
| _text, new_line, text = text.partition("\n") |
| if _text: |
| append(cls(_text, style)) |
| if new_line: |
| cropped_line = adjust_line_length( |
| line, length, style=style, pad=pad |
| ) |
| if include_new_lines: |
| cropped_line.append(new_line_segment) |
| yield cropped_line |
| del line[:] |
| else: |
| append(segment) |
| if line: |
| yield adjust_line_length(line, length, style=style, pad=pad) |
| |
| @classmethod |
| def adjust_line_length( |
| cls, |
| line: List["Segment"], |
| length: int, |
| style: Optional[Style] = None, |
| pad: bool = True, |
| ) -> List["Segment"]: |
| """Adjust a line to a given width (cropping or padding as required). |
| |
| Args: |
| segments (Iterable[Segment]): A list of segments in a single line. |
| length (int): The desired width of the line. |
| style (Style, optional): The style of padding if used (space on the end). Defaults to None. |
| pad (bool, optional): Pad lines with spaces if they are shorter than `length`. Defaults to True. |
| |
| Returns: |
| List[Segment]: A line of segments with the desired length. |
| """ |
| line_length = sum(segment.cell_length for segment in line) |
| new_line: List[Segment] |
| |
| if line_length < length: |
| if pad: |
| new_line = line + [cls(" " * (length - line_length), style)] |
| else: |
| new_line = line[:] |
| elif line_length > length: |
| new_line = [] |
| append = new_line.append |
| line_length = 0 |
| for segment in line: |
| segment_length = segment.cell_length |
| if line_length + segment_length < length or segment.control: |
| append(segment) |
| line_length += segment_length |
| else: |
| text, segment_style, _ = segment |
| text = set_cell_size(text, length - line_length) |
| append(cls(text, segment_style)) |
| break |
| else: |
| new_line = line[:] |
| return new_line |
| |
| @classmethod |
| def get_line_length(cls, line: List["Segment"]) -> int: |
| """Get the length of list of segments. |
| |
| Args: |
| line (List[Segment]): A line encoded as a list of Segments (assumes no '\\\\n' characters), |
| |
| Returns: |
| int: The length of the line. |
| """ |
| _cell_len = cell_len |
| return sum(_cell_len(segment.text) for segment in line) |
| |
| @classmethod |
| def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]: |
| """Get the shape (enclosing rectangle) of a list of lines. |
| |
| Args: |
| lines (List[List[Segment]]): A list of lines (no '\\\\n' characters). |
| |
| Returns: |
| Tuple[int, int]: Width and height in characters. |
| """ |
| get_line_length = cls.get_line_length |
| max_width = max(get_line_length(line) for line in lines) if lines else 0 |
| return (max_width, len(lines)) |
| |
| @classmethod |
| def set_shape( |
| cls, |
| lines: List[List["Segment"]], |
| width: int, |
| height: Optional[int] = None, |
| style: Optional[Style] = None, |
| new_lines: bool = False, |
| ) -> List[List["Segment"]]: |
| """Set the shape of a list of lines (enclosing rectangle). |
| |
| Args: |
| lines (List[List[Segment]]): A list of lines. |
| width (int): Desired width. |
| height (int, optional): Desired height or None for no change. |
| style (Style, optional): Style of any padding added. |
| new_lines (bool, optional): Padded lines should include "\n". Defaults to False. |
| |
| Returns: |
| List[List[Segment]]: New list of lines. |
| """ |
| _height = height or len(lines) |
| |
| blank = ( |
| [cls(" " * width + "\n", style)] if new_lines else [cls(" " * width, style)] |
| ) |
| |
| adjust_line_length = cls.adjust_line_length |
| shaped_lines = lines[:_height] |
| shaped_lines[:] = [ |
| adjust_line_length(line, width, style=style) for line in lines |
| ] |
| if len(shaped_lines) < _height: |
| shaped_lines.extend([blank] * (_height - len(shaped_lines))) |
| return shaped_lines |
| |
| @classmethod |
| def align_top( |
| cls: Type["Segment"], |
| lines: List[List["Segment"]], |
| width: int, |
| height: int, |
| style: Style, |
| new_lines: bool = False, |
| ) -> List[List["Segment"]]: |
| """Aligns lines to top (adds extra lines to bottom as required). |
| |
| Args: |
| lines (List[List[Segment]]): A list of lines. |
| width (int): Desired width. |
| height (int, optional): Desired height or None for no change. |
| style (Style): Style of any padding added. |
| new_lines (bool, optional): Padded lines should include "\n". Defaults to False. |
| |
| Returns: |
| List[List[Segment]]: New list of lines. |
| """ |
| extra_lines = height - len(lines) |
| if not extra_lines: |
| return lines[:] |
| lines = lines[:height] |
| blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) |
| lines = lines + [[blank]] * extra_lines |
| return lines |
| |
| @classmethod |
| def align_bottom( |
| cls: Type["Segment"], |
| lines: List[List["Segment"]], |
| width: int, |
| height: int, |
| style: Style, |
| new_lines: bool = False, |
| ) -> List[List["Segment"]]: |
| """Aligns render to bottom (adds extra lines above as required). |
| |
| Args: |
| lines (List[List[Segment]]): A list of lines. |
| width (int): Desired width. |
| height (int, optional): Desired height or None for no change. |
| style (Style): Style of any padding added. Defaults to None. |
| new_lines (bool, optional): Padded lines should include "\n". Defaults to False. |
| |
| Returns: |
| List[List[Segment]]: New list of lines. |
| """ |
| extra_lines = height - len(lines) |
| if not extra_lines: |
| return lines[:] |
| lines = lines[:height] |
| blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) |
| lines = [[blank]] * extra_lines + lines |
| return lines |
| |
| @classmethod |
| def align_middle( |
| cls: Type["Segment"], |
| lines: List[List["Segment"]], |
| width: int, |
| height: int, |
| style: Style, |
| new_lines: bool = False, |
| ) -> List[List["Segment"]]: |
| """Aligns lines to middle (adds extra lines to above and below as required). |
| |
| Args: |
| lines (List[List[Segment]]): A list of lines. |
| width (int): Desired width. |
| height (int, optional): Desired height or None for no change. |
| style (Style): Style of any padding added. |
| new_lines (bool, optional): Padded lines should include "\n". Defaults to False. |
| |
| Returns: |
| List[List[Segment]]: New list of lines. |
| """ |
| extra_lines = height - len(lines) |
| if not extra_lines: |
| return lines[:] |
| lines = lines[:height] |
| blank = cls(" " * width + "\n", style) if new_lines else cls(" " * width, style) |
| top_lines = extra_lines // 2 |
| bottom_lines = extra_lines - top_lines |
| lines = [[blank]] * top_lines + lines + [[blank]] * bottom_lines |
| return lines |
| |
| @classmethod |
| def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: |
| """Simplify an iterable of segments by combining contiguous segments with the same style. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable of segments. |
| |
| Returns: |
| Iterable[Segment]: A possibly smaller iterable of segments that will render the same way. |
| """ |
| iter_segments = iter(segments) |
| try: |
| last_segment = next(iter_segments) |
| except StopIteration: |
| return |
| |
| _Segment = Segment |
| for segment in iter_segments: |
| if last_segment.style == segment.style and not segment.control: |
| last_segment = _Segment( |
| last_segment.text + segment.text, last_segment.style |
| ) |
| else: |
| yield last_segment |
| last_segment = segment |
| yield last_segment |
| |
| @classmethod |
| def strip_links(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: |
| """Remove all links from an iterable of styles. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable segments. |
| |
| Yields: |
| Segment: Segments with link removed. |
| """ |
| for segment in segments: |
| if segment.control or segment.style is None: |
| yield segment |
| else: |
| text, style, _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. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable segments. |
| |
| Yields: |
| Segment: Segments with styles replace with None |
| """ |
| for text, _style, control in segments: |
| yield cls(text, None, control) |
| |
| @classmethod |
| def remove_color(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: |
| """Remove all color from an iterable of segments. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable segments. |
| |
| Yields: |
| Segment: Segments with colorless style. |
| """ |
| |
| cache: Dict[Style, Style] = {} |
| for text, style, control in segments: |
| if style: |
| colorless_style = cache.get(style) |
| if colorless_style is None: |
| colorless_style = style.without_color |
| cache[style] = colorless_style |
| yield cls(text, colorless_style, control) |
| else: |
| yield cls(text, None, control) |
| |
| @classmethod |
| def divide( |
| cls, segments: Iterable["Segment"], cuts: Iterable[int] |
| ) -> Iterable[List["Segment"]]: |
| """Divides an iterable of segments in to portions. |
| |
| Args: |
| cuts (Iterable[int]): Cell positions where to divide. |
| |
| Yields: |
| [Iterable[List[Segment]]]: An iterable of Segments in List. |
| """ |
| split_segments: List["Segment"] = [] |
| add_segment = split_segments.append |
| |
| iter_cuts = iter(cuts) |
| |
| while True: |
| try: |
| cut = next(iter_cuts) |
| except StopIteration: |
| return [] |
| if cut != 0: |
| break |
| yield [] |
| pos = 0 |
| |
| for segment in segments: |
| while segment.text: |
| end_pos = pos + segment.cell_length |
| if end_pos < cut: |
| add_segment(segment) |
| pos = end_pos |
| break |
| |
| try: |
| if end_pos == cut: |
| add_segment(segment) |
| yield split_segments[:] |
| del split_segments[:] |
| pos = end_pos |
| break |
| else: |
| before, segment = segment.split_cells(cut - pos) |
| add_segment(before) |
| yield split_segments[:] |
| del split_segments[:] |
| pos = cut |
| finally: |
| try: |
| cut = next(iter_cuts) |
| except StopIteration: |
| if split_segments: |
| yield split_segments[:] |
| return |
| yield split_segments[:] |
| |
| |
| class Segments: |
| """A simple renderable to render an iterable of segments. This class may be useful if |
| you want to print segments outside of a __rich_console__ method. |
| |
| Args: |
| segments (Iterable[Segment]): An iterable of segments. |
| new_lines (bool, optional): Add new lines between segments. Defaults to False. |
| """ |
| |
| def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None: |
| self.segments = list(segments) |
| self.new_lines = new_lines |
| |
| def __rich_console__( |
| self, console: "Console", options: "ConsoleOptions" |
| ) -> "RenderResult": |
| if self.new_lines: |
| line = Segment.line() |
| for segment in self.segments: |
| yield segment |
| yield line |
| else: |
| yield from self.segments |
| |
| |
| class SegmentLines: |
| def __init__(self, lines: Iterable[List[Segment]], new_lines: bool = False) -> None: |
| """A simple renderable containing a number of lines of segments. May be used as an intermediate |
| in rendering process. |
| |
| Args: |
| lines (Iterable[List[Segment]]): Lists of segments forming lines. |
| new_lines (bool, optional): Insert new lines after each line. Defaults to False. |
| """ |
| self.lines = list(lines) |
| self.new_lines = new_lines |
| |
| def __rich_console__( |
| self, console: "Console", options: "ConsoleOptions" |
| ) -> "RenderResult": |
| if self.new_lines: |
| new_line = Segment.line() |
| for line in self.lines: |
| yield from line |
| yield new_line |
| else: |
| for line in self.lines: |
| yield from line |
| |
| |
| if __name__ == "__main__": |
| |
| if __name__ == "__main__": # pragma: no cover |
| from pip._vendor.rich.console import Console |
| from pip._vendor.rich.syntax import Syntax |
| from pip._vendor.rich.text import Text |
| |
| code = """from rich.console import Console |
| console = Console() |
| text = Text.from_markup("Hello, [bold magenta]World[/]!") |
| console.print(text)""" |
| |
| text = Text.from_markup("Hello, [bold magenta]World[/]!") |
| |
| console = Console() |
| |
| console.rule("rich.Segment") |
| console.print( |
| "A Segment is the last step in the Rich render process before generating text with ANSI codes." |
| ) |
| console.print("\nConsider the following code:\n") |
| console.print(Syntax(code, "python", line_numbers=True)) |
| console.print() |
| console.print( |
| "When you call [b]print()[/b], Rich [i]renders[/i] the object in to the the following:\n" |
| ) |
| fragments = list(console.render(text)) |
| console.print(fragments) |
| console.print() |
| console.print( |
| "The Segments are then processed to produce the following output:\n" |
| ) |
| console.print(text) |
| console.print( |
| "\nYou will only need to know this if you are implementing your own Rich renderables." |
| ) |