Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control codes and Layout #1119

Merged
merged 15 commits into from Mar 25, 2021
30 changes: 30 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [10.0.0] - Unreleased

### Changed

- Made pydoc import lazy as at least one use found it slow to import https://github.com/willmcgugan/rich/issues/1104
- Modified string highlighting to not match in the middle of a word, so that apostrophes are not considered strings
- New way of encoding control codes in Segment
- New signature for Control class
- Changed Layout.split to use new Splitter class
- Improved layout.tree
- Changed default theme color for repr.number to cyan
- `__rich_measure__` signature changed to accept ConsoleOptions rather than max_width

### Added

- Added `__rich_repr__` protocol method to Pretty
- Added rich.region.Region
- Added ConsoleOptions.update_dimensions
- Added rich.console.ScreenUpdate
- Added Console.is_alt_screen
- Added Control.segment, Control.bell, Control.home, Control.move_to, Control.clear, Control.show_cursor, Control.alt_screen
- Added Console.update_screen and Console.update_screen_lines
- Added Layout.add_split, Layout.split_column, Layout.split_row, layout.refresh
- Added new Rich repr protocol `__rich_repr__`

### Fixed

- Fixed table style taking precedence over row style https://github.com/willmcgugan/rich/issues/1129
- Fixed incorrect measurement of Text with new lines and whitespace https://github.com/willmcgugan/rich/issues/1133

## [9.13.0] - 2021-03-06

### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/source/introduction.rst
Expand Up @@ -86,8 +86,8 @@ You can also use this feature to try out Rich *renderables*. Here's an example::
Read on to learn more about Rich renderables.


Rich Inspector
--------------
Rich Inspect
------------

Rich has an :meth:`~rich.inspect` function which can generate a report on any Python object. It is a fantastic debug aid, and a good example of the output that Rich can generate. Here is a simple example::

Expand Down
13 changes: 5 additions & 8 deletions docs/source/layout.rst
Expand Up @@ -18,25 +18,22 @@ To define a layout, construct a Layout object and print it::
layout = Layout()
print(layout)

This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, let's create a more interesting layout by calling the :meth:`~rich.layout.Layout.split` method to divide the layout in to two sub-layouts::
This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, let's create a more interesting layout by calling the :meth:`~rich.layout.Layout.split_column` method to divide the layout in to two sub-layouts::

layout.split(
layout.split_column(
Layout(name="upper"),
Layout(name="lower")
)
print(layout)

This will divide the terminal screen in to two equal sized portions, one on top of the other. The ``name`` attribute is an internal identifier we can use to look up the sub-layout later. Let's use that to create another split::
This will divide the terminal screen in to two equal sized portions, one on top of the other. The ``name`` attribute is an internal identifier we can use to look up the sub-layout later. Let's use that to create another split, this time we will call :meth:`~rich.layout.Layout.split_row` to split the lower layout in to a row of two sub-layouts.

layout["lower"].split(
layout["lower"].split_row(
Layout(name="left"),
Layout(name="right"),
direction="horizontal"
Layout(name="right"),
)
print(layout)

The addition of the ``direction="horizontal"`` tells the Layout class to split left-to-right, rather than the default of top-to-bottom.

You should now see the screen area divided in to 3 portions; an upper half and a lower half that is split in to two quarters.

.. raw:: html
Expand Down
6 changes: 3 additions & 3 deletions docs/source/protocol.rst
Expand Up @@ -64,10 +64,10 @@ For complete control over how a custom object is rendered to the terminal, you c
Measuring Renderables
~~~~~~~~~~~~~~~~~~~~~

Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class, for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the renderable objects in the Rich module, you will need to supply a ``__rich_measure__`` method which accepts a :class:`~rich.console.Console` and the maximum width and returns a :class:`~rich.measure.Measurement` object. The Measurement object should contain the *minimum* and *maximum* number of characters required to render.
Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class, for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the renderable objects in the Rich module, you will need to supply a ``__rich_measure__`` method which accepts a :class:`~rich.console.Console` and :class:`~rich.console.ConsoleOptions` and returns a :class:`~rich.measure.Measurement` object. The Measurement object should contain the *minimum* and *maximum* number of characters required to render.

For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board)::

class ChessBoard:
def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
return Measurement(8, max_width)
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
return Measurement(8, options.max_width)
3 changes: 1 addition & 2 deletions examples/fullscreen.py
Expand Up @@ -27,10 +27,9 @@ def make_layout() -> Layout:
Layout(name="main", ratio=1),
Layout(name="footer", size=7),
)
layout["main"].split(
layout["main"].split_row(
Layout(name="side"),
Layout(name="body", ratio=2, minimum_size=60),
direction="horizontal",
)
layout["side"].split(Layout(name="box1"), Layout(name="box2"))
return layout
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Expand Up @@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "9.13.0"
version = "10.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
Expand Down
6 changes: 4 additions & 2 deletions rich/__main__.py
Expand Up @@ -30,8 +30,10 @@ def __rich_console__(
yield Segment("▄", Style(color=color, bgcolor=bgcolor))
yield Segment.line()

def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
return Measurement(1, max_width)
def __rich_measure__(
self, console: "Console", options: ConsoleOptions
) -> Measurement:
return Measurement(1, options.max_width)


def make_test_card() -> Table:
Expand Down
40 changes: 0 additions & 40 deletions rich/_inspect.py
Expand Up @@ -209,43 +209,3 @@ def safe_getattr(attr_name: str) -> Tuple[Any, Any]:
f"[i][b]{not_shown_count}[/b] attribute(s) not shown.[/i] Run [b][red]inspect[/red]([not b]inspect[/])[/b] for options."
)
)


if __name__ == "__main__": # type: ignore
from rich import print

inspect = Inspect({}, docs=True, methods=True, dunder=True)
print(inspect)

t = Text("Hello, World")
print(Inspect(t))

from rich.style import Style
from rich.color import Color

print(Inspect(Style.parse("bold red on black"), methods=True, docs=True))
print(Inspect(Color.parse("#ffe326"), methods=True, docs=True))

from rich import get_console

print(Inspect(get_console(), methods=False))

print(Inspect(open("foo.txt", "wt"), methods=False))

print(Inspect("Hello", methods=False, dunder=True))
print(Inspect(inspect, methods=False, dunder=False, docs=False))

class Foo:
@property
def broken(self):
1 / 0

f = Foo()
print(Inspect(f))

print(Inspect(object, dunder=True))

print(Inspect(None, dunder=False))

print(Inspect(str, help=True))
print(Inspect(1, help=False))
30 changes: 24 additions & 6 deletions rich/_ratio.py
@@ -1,4 +1,5 @@
from math import ceil, modf
from fractions import Fraction
from math import ceil, floor, modf
from typing import cast, List, Optional, Sequence
from typing_extensions import Protocol

Expand Down Expand Up @@ -30,6 +31,8 @@ def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# Size of edge or None for yet to be determined
sizes = [(edge.size or None) for edge in edges]

_Fraction = Fraction

# While any edges haven't been calculated
while None in sizes:
# Get flexible edges and index to map these back on to sizes list
Expand All @@ -47,7 +50,9 @@ def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
for size, edge in zip(sizes, edges)
]
# Calculate number of characters in a ratio portion
portion = remaining / sum((edge.ratio or 1) for _, edge in flexible_edges)
portion = _Fraction(
remaining, sum((edge.ratio or 1) for _, edge in flexible_edges)
)

# If any edges will be less than their minimum, replace size with the minimum
for index, edge in flexible_edges:
Expand All @@ -59,11 +64,10 @@ def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
# Distribute flexible space and compensate for rounding error
# Since edge sizes can only be integers we need to add the remainder
# to the following line
_modf = modf
remainder = 0.0
remainder = _Fraction(0)
for index, edge in flexible_edges:
remainder, size = _modf(portion * edge.ratio + remainder)
sizes[index] = int(size)
size, remainder = divmod(portion * edge.ratio + remainder, 1)
sizes[index] = size
break
# Sizes now contains integers only
return cast(List[int], sizes)
Expand Down Expand Up @@ -135,3 +139,17 @@ def ratio_distribute(
total_ratio -= ratio
total_remaining -= distributed
return distributed_total


if __name__ == "__main__": # type: ignore
from dataclasses import dataclass

@dataclass
class E:

size: Optional[int] = None
ratio: int = 1
minimum_size: int = 1

resolved = ratio_resolve(110, [E(None, 1, 1), E(None, 1, 1), E(None, 1, 1)])
print(sum(resolved))
14 changes: 9 additions & 5 deletions rich/align.py
Expand Up @@ -133,7 +133,7 @@ def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
align = self.align
width = Measurement.get(console, self.renderable).maximum
width = Measurement.get(console, options, self.renderable).maximum
rendered = console.render(
Constrain(
self.renderable, width if self.width is None else min(width, self.width)
Expand Down Expand Up @@ -221,8 +221,10 @@ def blank_lines(count) -> Iterable[Segment]:
iter_segments = Segment.apply_style(iter_segments, style)
yield from iter_segments

def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
measurement = Measurement.get(console, self.renderable, max_width)
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement


Expand Down Expand Up @@ -275,8 +277,10 @@ def blank_lines(count) -> Iterable[Segment]:
if bottom_space > 0:
yield from blank_lines(bottom_space)

def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
measurement = Measurement.get(console, self.renderable, max_width)
def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement


Expand Down
11 changes: 8 additions & 3 deletions rich/bar.py
Expand Up @@ -49,7 +49,10 @@ def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:

width = min(self.width or options.max_width, options.max_width)
width = min(
self.width if self.width is not None else options.max_width,
options.max_width,
)

if self.begin >= self.end:
yield Segment(" " * width, self.style)
Expand Down Expand Up @@ -81,9 +84,11 @@ def __rich_console__(
yield Segment(prefix + body[len(prefix) :] + suffix, self.style)
yield Segment.line()

def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return (
Measurement(self.width, self.width)
if self.width is not None
else Measurement(4, max_width)
else Measurement(4, options.max_width)
)
2 changes: 1 addition & 1 deletion rich/columns.py
Expand Up @@ -77,7 +77,7 @@ def __rich_console__(

get_measurement = Measurement.get
renderable_widths = [
get_measurement(console, renderable, max_width).maximum
get_measurement(console, options, renderable).maximum
for renderable in renderables
]
if self.equal:
Expand Down