panel title

This commit is contained in:
Will McGugan 2020-07-12 16:17:36 +01:00
parent 17bcbf56f2
commit 9f165b5b06
13 changed files with 206 additions and 31 deletions

View File

@ -5,6 +5,16 @@ 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).
## [3.3.0] - 2020-07-12
### Added
- Added title and title_align options to Panel
- Added pad and width parameters to Align
- Added end parameter to Rule
- Added Text.pad and Text.align methods
- Added leading parameter to Table
## [3.2.0] - 2020-07-10
### Added

View File

@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "3.2.0"
version = "3.3.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"

View File

@ -1,6 +1,7 @@
from typing import TYPE_CHECKING
from typing import Iterable, TYPE_CHECKING
from typing_extensions import Literal
from .constrain import Constrain
from .jupyter import JupyterMixin
from .measure import Measurement
from .segment import Segment
@ -20,13 +21,21 @@ class Align(JupyterMixin):
renderable (RenderableType): A console renderable.
align (AlignValues): One of "left", "center", or "right""
style (StyleType, optional): An optional style to apply to the renderable.
pad (bool, optional): Pad the right with spaces. Defaults to True.
width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
Raises:
ValueError: if ``align`` is not one of the expected values.
"""
def __init__(
self, renderable: "RenderableType", align: AlignValues, style: StyleType = None
self,
renderable: "RenderableType",
align: AlignValues,
style: StyleType = None,
*,
pad: bool = True,
width: int = None,
) -> None:
if align not in ("left", "center", "right"):
raise ValueError(
@ -35,35 +44,58 @@ class Align(JupyterMixin):
self.renderable = renderable
self.align = align
self.style = style
self.pad = pad
self.width = width
@classmethod
def left(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
def left(
cls,
renderable: "RenderableType",
style: StyleType = None,
*,
pad: bool = True,
width: int = None,
) -> "Align":
"""Align a renderable to the left."""
return cls(renderable, "left", style=style)
return cls(renderable, "left", style=style, pad=pad, width=width)
@classmethod
def center(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
def center(
cls,
renderable: "RenderableType",
style: StyleType = None,
*,
pad: bool = True,
width: int = None,
) -> "Align":
"""Align a renderable to the center."""
return cls(renderable, "center", style=style)
return cls(renderable, "center", style=style, pad=pad, width=width)
@classmethod
def right(cls, renderable: "RenderableType", style: StyleType = None) -> "Align":
def right(
cls,
renderable: "RenderableType",
style: StyleType = None,
*,
pad: bool = True,
width: int = None,
) -> "Align":
"""Align a renderable to the right."""
return cls(renderable, "right", style=style)
return cls(renderable, "right", style=style, pad=pad, width=width)
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
align = self.align
rendered = console.render(self.renderable, options)
rendered = console.render(Constrain(self.renderable, width=self.width), options)
lines = list(Segment.split_lines(rendered))
width, height = Segment.get_shape(lines)
lines = Segment.set_shape(lines, width, height)
new_line = Segment.line()
excess_space = options.max_width - width
def generate_segments():
def generate_segments() -> Iterable[Segment]:
if excess_space <= 0:
# Exact fit
for line in lines:
@ -72,17 +104,18 @@ class Align(JupyterMixin):
elif align == "left":
# Pad on the right
pad = Segment(" " * excess_space)
pad = Segment(" " * excess_space) if self.pad else None
for line in lines:
yield from line
yield pad
if pad:
yield pad
yield new_line
elif align == "center":
# Pad left and right
left = excess_space // 2
pad = Segment(" " * left)
pad_right = Segment(" " * (excess_space - left))
pad_right = Segment(" " * (excess_space - left)) if self.pad else None
for line in lines:
if left:
yield pad

View File

@ -102,6 +102,11 @@ class Box:
horizontal = self.row_horizontal
cross = self.row_cross
right = self.row_right
elif level == "mid":
left = self.mid_left
horizontal = " "
cross = self.mid_vertical
right = self.mid_right
elif level == "foot":
left = self.foot_row_left
horizontal = self.foot_row_horizontal

View File

@ -1,9 +1,11 @@
from typing import Optional
from typing import Optional, TYPE_CHECKING
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderableType, RenderResult
class Constrain(JupyterMixin):
"""Constrain the width of a renderable to a given number of characters.
@ -13,20 +15,20 @@ class Constrain(JupyterMixin):
width (int, optional): The maximum width (in characters) to render. Defaults to 80.
"""
def __init__(self, renderable: RenderableType, width: Optional[int] = 80) -> None:
def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None:
self.renderable = renderable
self.width = width
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.width is None:
yield self.renderable
else:
child_options = options.update(width=min(self.width, options.max_width))
yield from console.render(self.renderable, child_options)
def __rich_measure__(self, console: Console, max_width: int) -> Measurement:
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
if self.width is None:
return Measurement.get(console, self.renderable, max_width)
else:

View File

@ -2,6 +2,7 @@ from typing import Optional, Union
from .box import get_safe_box, Box, SQUARE, ROUNDED
from .align import AlignValues
from .console import (
Console,
ConsoleOptions,
@ -12,7 +13,7 @@ from .console import (
from .jupyter import JupyterMixin
from .padding import Padding, PaddingDimensions
from .style import Style, StyleType
from .text import Text
from .text import Text, TextType
from .segment import Segment
@ -40,6 +41,8 @@ class Panel(JupyterMixin):
renderable: RenderableType,
box: Box = ROUNDED,
*,
title: TextType = None,
title_align: AlignValues = "center",
safe_box: Optional[bool] = None,
expand: bool = True,
style: StyleType = "none",
@ -49,6 +52,8 @@ class Panel(JupyterMixin):
) -> None:
self.renderable = renderable
self.box = box
self.title = title
self.title_align = title_align
self.safe_box = safe_box
self.expand = expand
self.style = style
@ -62,6 +67,8 @@ class Panel(JupyterMixin):
renderable: RenderableType,
box: Box = ROUNDED,
*,
title: TextType = None,
title_align: AlignValues = "center",
safe_box: Optional[bool] = None,
style: StyleType = "none",
border_style: StyleType = "none",
@ -72,6 +79,8 @@ class Panel(JupyterMixin):
return cls(
renderable,
box,
title=title,
title_align=title_align,
safe_box=safe_box,
style=style,
border_style=border_style,
@ -108,7 +117,24 @@ class Panel(JupyterMixin):
line_start = Segment(box.mid_left, border_style)
line_end = Segment(f"{box.mid_right}", border_style)
new_line = Segment.line()
yield Segment(box.get_top([width - 2]), border_style)
if self.title is None:
yield Segment(box.get_top([width - 2]), border_style)
else:
title_text = (
Text.from_markup(self.title)
if isinstance(self.title, str)
else self.title.copy()
)
title_text.style = border_style
title_text.end = ""
title_text.plain = title_text.plain.replace("\n", " ")
title_text = title_text.tabs_to_spaces()
title_text.pad(1)
title_text.align(self.title_align, width - 4, character=box.top)
yield Segment(box.top_left + box.top, border_style)
yield title_text
yield Segment(box.top + box.top_right, border_style)
yield new_line
for line in lines:
yield line_start
@ -129,7 +155,7 @@ if __name__ == "__main__": # pragma: no cover
c = Console()
from .padding import Padding
from .box import ROUNDED
from .box import ROUNDED, DOUBLE
p = Panel(
Panel.fit(
@ -138,7 +164,8 @@ if __name__ == "__main__": # pragma: no cover
safe_box=True,
style="on red",
),
border_style="on blue",
title="[b]Hello, World",
box=DOUBLE,
)
print(p)

View File

@ -8,11 +8,13 @@ from .text import Text
class Rule(JupyterMixin):
"""A console renderable to draw a horizontal rule (line).
r"""A console renderable to draw a horizontal rule (line).
Args:
title (Union[str, Text], optional): Text to render in the rule. Defaults to "".
character (str, optional): Character used to draw the line. Defaults to "".
style (StyleType, optional): Style of Rule. Defaults to "rule.line".
end (str, optional): Character at end of Rule. defaults to "\\n"
"""
def __init__(
@ -21,12 +23,14 @@ class Rule(JupyterMixin):
*,
character: str = None,
style: Union[str, Style] = "rule.line",
end: str = "\n",
) -> None:
if character and cell_len(character) != 1:
raise ValueError("'character' argument must have a cell width of 1")
self.title = title
self.character = character
self.style = style
self.end = end
def __repr__(self) -> str:
return f"Rule({self.title!r}, {self.character!r})"
@ -51,7 +55,7 @@ class Rule(JupyterMixin):
title_text.plain = title_text.plain.replace("\n", " ")
title_text = title_text.tabs_to_spaces()
rule_text = Text()
rule_text = Text(end=self.end)
center = (width - cell_len(title_text.plain)) // 2
rule_text.append(character * (center - 1) + " ", self.style)
rule_text.append(title_text)

View File

@ -109,6 +109,7 @@ class Table(JupyterMixin):
show_footer (bool, optional): Show a footer row. Defaults to False.
show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True.
show_lines (bool, optional): Draw lines between every row. Defaults to False.
leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to False.
style (Union[str, Style], optional): Default style for the table. Defaults to "none".
row_styles (List[Union, str], optional): Optional list of row styles, if more that one style is give then the styles will alternate. Defaults to None.
header_style (Union[str, Style], optional): Style of the header. Defaults to None.
@ -136,6 +137,7 @@ class Table(JupyterMixin):
show_footer: bool = False,
show_edge: bool = True,
show_lines: bool = False,
leading: int = 0,
style: StyleType = "none",
row_styles: Iterable[StyleType] = None,
header_style: StyleType = None,
@ -161,6 +163,7 @@ class Table(JupyterMixin):
self.show_footer = show_footer
self.show_edge = show_edge
self.show_lines = show_lines
self.leading = leading
self.collapse_padding = collapse_padding
self.style = style
self.header_style = header_style
@ -593,6 +596,7 @@ class Table(JupyterMixin):
show_footer = self.show_footer
show_edge = self.show_edge
show_lines = self.show_lines
leading = self.leading
_Segment = Segment
if _box:
@ -685,15 +689,22 @@ class Table(JupyterMixin):
_box.get_row(widths, "head", edge=show_edge), border_style
)
yield new_line
if _box and show_lines:
if _box and (show_lines or leading):
if (
not last
and not (show_footer and index >= len(rows) - 2)
and not (show_header and header_row)
):
yield _Segment(
_box.get_row(widths, "row", edge=show_edge), border_style
)
if leading:
for _ in range(leading):
yield _Segment(
_box.get_row(widths, "mid", edge=show_edge),
border_style,
)
else:
yield _Segment(
_box.get_row(widths, "row", edge=show_edge), border_style
)
yield new_line
if _box and show_edge:

View File

@ -16,6 +16,7 @@ from typing import (
from ._loop import loop_last
from ._wrap import divide_line
from .align import AlignValues
from .cells import cell_len, set_cell_size
from .containers import Lines
from .control import strip_control_codes
@ -617,6 +618,15 @@ class Text(JupyterMixin):
append(span)
self._spans[:] = spans
def pad(self, count: int, character: str = " ") -> None:
"""Pad left and right with a given number of characters.
Args:
count (int): Width of padding.
"""
self.pad_left(count, character=character)
self.pad_right(count, character=character)
def pad_left(self, count: int, character: str = " ") -> None:
"""Pad the left with a given character.
@ -640,6 +650,26 @@ class Text(JupyterMixin):
if count:
self.plain = f"{self.plain}{character * count}"
def align(self, align: AlignValues, width: int, character: str = " ") -> None:
"""Align text to a given width.
Args:
align (AlignValues): One of "left", "center", or "right".
width (int): Desired width.
character (str, optional): Character to pad with. Defaults to " ".
"""
self.truncate(width)
excess_space = width - cell_len(self.plain)
if excess_space:
if align == "left":
self.pad_right(excess_space, character)
elif align == "center":
left = excess_space // 2
self.pad_left(left, character)
self.pad_right(excess_space - left, character)
else:
self.pad_left(excess_space, character)
def append(
self, text: Union["Text", str], style: Union[str, "Style"] = None
) -> "Text":

View File

@ -64,6 +64,22 @@ def test_measure():
assert _max == 7
def test_align_no_pad():
console = Console(file=io.StringIO(), width=10)
console.print(Align("foo", "center", pad=False))
console.print(Align("foo", "left", pad=False))
assert console.file.getvalue() == " foo\nfoo\n"
def test_align_width():
console = Console(file=io.StringIO(), width=40)
words = "Deep in the human unconscious is a pervasive need for a logical universe that makes sense. But the real universe is always one step beyond logic"
console.print(Align(words, "center", width=30))
result = console.file.getvalue()
expected = " Deep in the human unconscious \n is a pervasive need for a \n logical universe that makes \n sense. But the real universe \n is always one step beyond \n logic \n"
assert result == expected
def test_shortcuts():
assert Align.left("foo").align == "left"
assert Align.left("foo").renderable == "foo"

View File

@ -10,6 +10,7 @@ tests = [
Panel.fit("Hello, World"),
Panel("Hello, World", width=8),
Panel(Panel("Hello, World")),
Panel("Hello, World", title="FOO"),
]
expected = [
@ -18,6 +19,7 @@ expected = [
"╭────────────╮\n│Hello, World│\n╰────────────╯\n",
"╭──────╮\n│Hello,│\n│World │\n╰──────╯\n",
"╭────────────────────────────────────────────────╮\n│╭──────────────────────────────────────────────╮│\n││Hello, World ││\n│╰──────────────────────────────────────────────╯│\n╰────────────────────────────────────────────────╯\n",
"╭───────────────────── FOO ──────────────────────╮\n│Hello, World │\n╰────────────────────────────────────────────────╯\n",
]

File diff suppressed because one or more lines are too long

View File

@ -517,3 +517,27 @@ def test_truncate_ellipsis_pad(input, count, expected):
text = Text(input)
text.truncate(count, overflow="ellipsis", pad=True)
assert text.plain == expected
def test_pad():
test = Text("foo")
test.pad(2)
assert test.plain == " foo "
def test_align_left():
test = Text("foo")
test.align("left", 10)
assert test.plain == "foo "
def test_align_right():
test = Text("foo")
test.align("right", 10)
assert test.plain == " foo"
def test_align_center():
test = Text("foo")
test.align("center", 10)
assert test.plain == " foo "