mirror of https://github.com/Textualize/rich.git
panel title
This commit is contained in:
parent
17bcbf56f2
commit
9f165b5b06
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
30
rich/text.py
30
rich/text.py
|
@ -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":
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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 "
|
||||
|
|
Loading…
Reference in New Issue